20 Commits

Author SHA1 Message Date
e3713773b7 Save reposted reel id for future use
Some checks failed
Push image to registry / build-image (push) Failing after 52s
- Changed the managed account login from phone number, username or email to only username so it can be used for other things
2025-08-05 13:58:10 +02:00
aa936a2a11 Instagram jobs refactor + started InstagramNotifications 2025-08-05 13:25:25 +02:00
1f23b112d7 redo README 2025-08-05 13:11:49 +02:00
0aa34d170a Fix selenium user_profile folder ? 2025-08-03 12:46:00 +02:00
f0e52147e4 Added video transcription
All checks were successful
Push image to registry / build-image (push) Successful in 5m59s
2025-07-04 12:36:03 +02:00
a00c5ba4b8 Fix prompt, unescaped double quote
All checks were successful
Push image to registry / build-image (push) Successful in 4m40s
2025-07-02 13:43:45 +02:00
f9f1b8ed3d Fix prompt, unescaped double quot
Some checks failed
Push image to registry / build-image (push) Failing after 40s
2025-07-02 13:30:04 +02:00
f192cba1f8 Updated LLM prompts and System message
Some checks failed
Push image to registry / build-image (push) Failing after 51s
2025-07-02 13:20:42 +02:00
6b9b5a60e9 Should fix HellcaseJob login
All checks were successful
Push image to registry / build-image (push) Successful in 3m21s
2025-07-02 12:29:36 +02:00
6d92eb76d8 Fix InstagramRepostJob describing non downloaded reel 2025-07-02 12:24:50 +02:00
fcc78fd560 fix tesseract
All checks were successful
Push image to registry / build-image (push) Successful in 4m44s
2025-07-01 11:57:51 +02:00
a57cbffbeb Add LLM env variables 2025-07-01 11:29:35 +02:00
4623a52bcc Fix tesseract dependencies
All checks were successful
Push image to registry / build-image (push) Successful in 4m55s
2025-07-01 11:09:14 +02:00
8ab097ca1c Install Tesseract dependencies
Some checks failed
Push image to registry / build-image (push) Failing after 39s
2025-07-01 11:08:20 +02:00
9d0a1b5cf9 Fix
All checks were successful
Push image to registry / build-image (push) Successful in 6m51s
2025-06-30 16:33:07 +02:00
77fcee7a83 Fix ?
Some checks failed
Push image to registry / build-image (push) Failing after 42s
2025-06-30 16:29:41 +02:00
1f7f4c665d Fix composer version
Some checks failed
Push image to registry / build-image (push) Failing after 44s
2025-06-30 16:24:09 +02:00
44d7d52f23 fix composer version 2025-06-30 16:23:47 +02:00
25b5b1be27 Merge branch 'jobs/instagram-repost/ai-description'
Some checks failed
Push image to registry / build-image (push) Failing after 1m23s
2025-06-30 16:14:57 +02:00
7054597696 Fix non downloaded videos
All checks were successful
Push image to registry / build-image (push) Successful in 4m56s
2025-06-09 18:30:38 +02:00
26 changed files with 929 additions and 240 deletions

View File

@ -81,3 +81,11 @@ VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
# AI LLM
LLM_API_HOST_URL=${LLM_API_HOST_URL}
LLM_API_TOKEN=${LLM_API_TOKEN}
LLM_CHAT_MODEL=${LLM_CHAT_MODEL}
LLM_CHAT_MODEL_THINK=${LLM_CHAT_MODEL_THINK}
LLM_VISION_MODEL=${LLM_VISION_MODEL}
LLM_VISION_MODEL_THINK=${LLM_VISION_MODEL_THINK}

View File

@ -83,6 +83,9 @@ VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
# AI LLM
LLM_HOST_URL="https://openai.com/api"
LLM_CHAT_MODEL="gpt-4o"
LLM_VISION_MODEL="gpt-4o-vision-preview"
LLM_API_HOST_URL="https://chat.myopen-webui.dev/ollama"
LLM_API_TOKEN="myopen-webui-token-1234567890abcdef" # Replace with your actual token
LLM_CHAT_MODEL="deepseek-r1:8b"
LLM_CHAT_MODEL_THINK=true
LLM_VISION_MODEL="llava:7b"
LLM_VISION_MODEL_THINK=false

View File

@ -1,6 +1,6 @@
# INSTALL PHP COMPOSER DEPENDENCIES
FROM composer:lts AS composer-deps
FROM composer:2.7.9 AS composer-deps
WORKDIR /
@ -61,6 +61,11 @@ RUN apk update && apk add --no-cache \
RUN docker-php-ext-configure zip && docker-php-ext-install zip
RUN docker-php-ext-install gd pdo pdo_mysql zip
# Tesseract-OCR module downloads
# Based on https://github.com/Franky1/docker-tesseract/blob/master/Dockerfile.main
RUN wget --no-check-certificate https://github.com/tesseract-ocr/tessdata/raw/refs/heads/main/eng.traineddata -P /usr/share/tessdata \
&& wget --no-check-certificate https://github.com/tesseract-ocr/tessdata/raw/refs/heads/main/fra.traineddata -P /usr/share/tessdata
# Install latest version of the linux binary of yt-dlp into /bin/yt-dlp
# Get the file from https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest/download/yt-dlp
RUN curl -L https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest/download/yt-dlp -o /bin/yt-dlp \

View File

@ -10,38 +10,194 @@ This method comes from the idea that the best way to prompt engineer is to ask t
# Prompts
Starting sentence is usually :
```
Starting sentence is usually :
```text
Im using some LLM and I would need a prompt and a system message for every use case I will give you.
Im using structured JSON output provided by the openAI API. The output structure is a simple {"type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"]}, so only “answer” can be filled. For the input, everything will be given in the prompt. Give me the system message and prompt separately, preferably in text format.
```
## Instagram
### Instagram Reel caption generation
```
Im using some LLM and I would need a prompt, a system message and an output format for every use case I will give you.
```text
Im using some LLM and I would need a prompt, a system message for every use case I will give you.
The first one is when Im trying to generate a caption for an instagram Reel. For the moment, I can give the LLM the original instagram reel caption that was downloaded from, and a description by an LLM of the video, or the joke behind it.
The caption must be short and well placed with the reel. For example, if the reel is funny, the caption must be short and funny, while still relating to the reel. The caption must not be describint the video like the LLM description does
The caption must be short and well placed with the reel. For example, if the reel is funny, the caption must be short and funny, while still relating to the reel. The caption must not be describing the video like the LLM description does (for example this bad example describe the content of the video instead of doing a caption based on the description given : “Three animated friends chilling in the woods at night until someone's phone inevitably starts ringing somewhere nearby... 😅🌲✨” or this one : “This reel shows me trying to make my sad texts shorter with ChatGPT, but it just frustrates me more! 😅😂”).
It also shouldnt begin with something like This reel…. For example this is a bad output : “This reel hilariously mocks every awkward fan reaction to those intense DCU movie scenes. 🎭 #DCFanDrama”
The LLM can add some appropriate hashtags if it wants to and seem appropriate.
Sometimes, the original caption will credit the original author, most of the times on twitter like (“credit : t/twitteruser”). Those credit can appear in the generated caption too, But I dont want any instagram account mention (“@instagramUser”) because most of the time its to incite to subscribe to the downloaded reel account. The use of emoji is encouraged, but not too much and it has to not look stupid or too.
Sometimes, the original caption will credit the original author, most of the times on twitter like (“credit : t/twitteruser”). Those credit can appear in the generated caption too, But I dont want any instagram account mention (“@instagramUser”) because usually its to incite to subscribe to the downloaded reel account (like “Seen me already ? follow me @instagramUser”). I dont want long credits too, juste a simple “credit tt/twitteraccount” is enough. Not like this bad example : “Credited via the brilliant mind at tt/batinterface!…”
The use of emoji is encouraged, but not too much and it has to not look stupid or too.
When using it, I encoutered some problems like this one :
“Credit to: [Original Creator] for this hilarious video game scene where the characters look suspiciously like Kermit the Frog! 😂”. The [Original creator] is not filled in, I dont even know if the original caption had one.
Also sometimes the results says something about the OCR, of course, it shouldn't say anything about the input being wrong like the OCR not making sense in the final answer. The entire text the LLM produces will be set as caption.
Some caption are just lame and feels like a facebook post. The intended audience here is young.
```
## Video Descriptor
Im using some LLM and I would need a prompt and a system message for every use case I will give you.
```text
Im using some LLM and I would need a prompt and a system message for every use case I will give you.
The LLM here will be used to describe an Instagram Reel (video). Each screenshot of that video will be described using an LLM, prompt, system message and output format. The description of all the screenshots will be given to this LLM that will try to recreate the video based on the description of the screenshots, and describe the video.
The required prompt here is for the LLM that will compile the description into one and try to understand the video and describe it. Im particularly interested in the joke behind the reel if there is one.
Im using structured JSON output provided by the openAI API. The output structure is a simple {"type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"]}, so only “answer” can be filled. For the input, everything will be given in the prompt. Give me the system message and prompt separately, preferably in text format.
This is an example of a screenshot description by an LLM : “The image shows a close-up of a person's hands holding what appears to be a brown object with a plastic covering, possibly food wrapped in paper or foil. There is also a small portion visible at the top right corner, which seems to be a red and white label. The focus of the image is on the hands holding the object.”
The LLM here will be used to describe an Instagram Reel (video). Each screenshot of that video will be described using an LLM, prompt, system message and output format. The description of all the screenshots will be given to this LLM that will try to recreate the video based on the description of the screenshots, and describe the video.
Most of the description wont make sense, so some details should be omitted. For example, one screenshot description could say the main subject is a car, and another one 3 seconds later in the video could say the main subject is a cat. You could say the car transformed into a cat, but it would be safer to assume that one of the description is wrong and the main characted was a cat all along the video because another description in the video also says the main subject is a cat.
It is safe to say that most analysed videos will be of bad quality. which means the screenshots description can vary a lot
The required prompt here is for the LLM that will compile the description into one and try to understand the video and describe it. Im particularly interested in the joke behind the reel if there is one.
This is an example of a screenshot description by an LLM : “The image shows a close-up of a person's hands holding what appears to be a brown object with a plastic covering, possibly food wrapped in paper or foil. There is also a small portion visible at the top right corner, which seems to be a red and white label. The focus of the image is on the hands holding the object.”
The informations given in the prompts are :
- An audio transcription of the full video
- For each of the screenshots :
The screenshot number ("Screenshot: 3" for example)
The timestamp in the video of when the screenshot is taken
An OCR result (may contain some weird character, the OCR is not filtered or cleansed), if not text is found, it mentions "No text found"
The LLM description of the screenshot
Here is an example of prompt given :
\```text
Audio Transcription: Hey !
Screenshot: 1
Timestamp: 0s
OCR: See
fia) oiled 5 genuinely fh
LLM Description: 1. Scene Description: The image shows a person standing inside a building, possibly a store or a commercial establishment. The individual is positioned in the middle of the frame and appears to be looking directly at the camera or the viewer. There are other people present as well, but they are not the main focus of this description. 2. Main Subject/Character(s): The primary subject is a person who seems to be trying to communicate with someone off-camera. They appear to be standing in line or waiting, and their posture suggests that they may be impatient or frustrated. 3. Text Description: There is visible text on the image, which reads as follows: 'when my day 1 try to dap me up but he's gonnily do it again when I step out that door love you os 4. Summary: The image captures a moment of frustration or impatience between two individuals in an indoor setting. 5. Joke: It seems there is no joke or humorous element present in this image.
Screenshot: 2
Timestamp: 2s
OCR: we v),®
When my Civ 1 tay to cep
movphwthes ganuinahy th
tune)witithatlovejisiand|ps)
(o
W
LLM Description: 1. **Scene Description:** The image shows an interior setting, which appears to be a spacious room with a high ceiling and a patterned floor. There is natural light coming in from the upper part of the space. The room seems to have some sort of event or gathering happening within it. 2. **Main Subject/Character(s):** The main subject is a person who is standing, walking across the room. They are wearing dark clothing and have their back turned towards the camera. 3. **Text Description (if any):** There is visible text overlaid on the image which reads
Screenshot: 3
Timestamp: 4s
OCR: a up erent a
ae i Li bs
ree
I
LLM Description: 1. Scene Description: The image appears to be a smartphone screenshot of a social media post, specifically an Instagram story. There is a person in the foreground, who seems to be outdoors. The individual is standing near a storefront with a visible display window featuring mannequins and merchandise. The sky is clear, suggesting it might be daytime. The background also shows other people walking on the sidewalk, which indicates that this is likely an urban area. The presence of the store and the sidewalk suggest that this scene takes place in a commercial or shopping district. There are texts at the top of the image that appear to be part of Instagram's interface elements. It seems that there might have been some interaction with the post, as indicated by the emoji reactions. The overall setting appears to be an urban environment during daytime. 2. Main Subject/Character(s): The main subject in this image is a person who is standing in front of a storefront. This individual appears to be engaged with their smartphone, possibly viewing or interacting with the post. It's not possible to provide specific details about the person beyond what they are wearing and how they are positioned within the frame. 3. Text Description (if any): The image contains several text elements that include emoji reactions and captions. There are emojis indicating various types of interactions, such as
Screenshot: 4
Timestamp: 6s
OCR: a day 1 try to.dap
but hes Ce in
RR OU oS
LLM Description: 1. **Scene Description:** The image shows an interior space that appears to be a shopping area or mall, with visible merchandise and store displays. A person is walking through the scene, seemingly captured mid-step while using their phone. The setting suggests a casual, everyday environment. 2. **Main Subject/Character(s):** The main subject is a person in mid-stride, looking down at their cell phone, possibly engaged with it. There are no additional characters or significant interactions depicted. 3. **Text Description (if any):** There is text overlaid on the image which reads,
Screenshot: 5
Timestamp: 8s
OCR: No text found
LLM Description: 1. Scene Description: The image depicts a person walking through an indoor shopping mall. It appears to be a public space, with visible storefronts and a ceiling-mounted security camera in the background. There is also an escalator in the scene, which suggests multiple levels to the building. The lighting and layout suggest a modern, clean design typical of contemporary malls. 2. Main Subject/Character(s): A young man is walking through the shopping mall, seemingly alone, with his head down. He appears to be engaged with something he's holding in his hand, possibly a mobile phone, which might imply that he is texting or looking at content on his device. 3. Text Description (if any): There is text overlaid on the image. It reads,
\```
Most of the description wont make sense, so some details should be omitted. For example, one screenshot description could say the main subject is a car, and another one 3 seconds later in the video could say the main subject is a cat. You could say the car transformed into a cat, but it would be safer to assume that one of the description is wrong and the main characted was a cat all along the video because another description in the video also says the main subject is a cat.
It is safe to say that most analysed videos will be of bad quality. which means the screenshots description can vary a lot.
Found text by OCR and screenshots descriptions can be passed to the final video description if it seems coherent.
```
### Screenshot descriptor
```
```text
Im using some LLM and I would need a prompt, a system message and an output format for every use case I will give you.
The first one must describe a screenshot from a video. Each screenshot of that video will be described using the same LLM, prompt, system message and output format. The description of all the screenshots will be given to another LLM that will try to recreate the video based on the description of the screenshots, and describe the video.
The required prompt here is the one that describes a screenshot. The LLM will only be given the screenshot as input information. I need the LLM to describe the given screenshot. No need to specify that it is a screenshot. The LLM description must include specify the scene, the character or the main subject, the text present on the screenshots, most of the time it will be caption added after video editing, that may use emojis.

View File

@ -1,66 +1,7 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
# DatBrowser
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## Development
## About Laravel
### Links
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
- [novnc](http://localhost:7900/?password=secret&autoconnect=true&scale=local)

View File

@ -161,7 +161,7 @@ abstract class BrowserJob implements ShouldQueue
'--disable-setuid-sandbox',
'--whitelisted-ips=""',
'--disable-dev-shm-usage',
'--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")
'--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(
@ -366,4 +366,16 @@ abstract class BrowserJob implements ShouldQueue
AllNotification::send(new JobDebugNotification($this->jobId, $errorMessage));
}
}
public function clickElementWithJavaScript(Browser $browser, string $xPathSelector): void
{
$browser->script("
var element = document.evaluate('{$xPathSelector}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (element) {
element.click();
} else {
console.error('Element not found: {$xPathSelector}');
}
");
}
}

View File

@ -99,7 +99,7 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
sleep(5);
$browser->waitForText("Sign in with Steam", 30, true);
sleep(3);
$browser->driver->findElement(WebDriverBy::xpath('//button[contains(@class,"_base_zvftr_1 _accent-1_zvftr_105 _m_zvftr_52 _full_zvftr_94 _primary_zvftr_100")]'))->click();
$browser->driver->findElement(WebDriverBy::xpath("//button[. = 'Sign in with Steam']"))->click();
sleep(5);
// QR CODE SCANNING

View File

@ -0,0 +1,112 @@
<?php
namespace App\Browser\Jobs\Instagram;
use App\Browser\BrowserJob;
use App\Browser\JobDebugScreenshot;
use App\Models\JobRun;
use App\Notification\Notifications\JobDebugNotification;
use App\Notification\Providers\AllNotification;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
use App\Services\AIPrompt\OpenAPIPrompt;
abstract class InstagramAbstractJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
{
// === CONFIGURATION ===
protected Collection $jobInfos;
protected JobRun $jobRun;
protected OpenAPIPrompt $openAPIPrompt;
public function __construct($jobId, OpenAPIPrompt $openAPIPrompt = null)
{
parent::__construct($jobId);
$this->openAPIPrompt = $openAPIPrompt ?? app(OpenAPIPrompt::class);
}
protected function clickNext(Browser $browser) {
$nextButton = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(text(), "Next") or contains(text(), "Share")]'));
$nextButton->click();
sleep(2);
}
protected function putOriginalResolution(Browser $browser)
{
try {
$chooseResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//button[./div/*[local-name() = "svg"][@aria-label="Select crop"]]'));
$chooseResolutionButton->click();
sleep(2);
// Choos "original" resolution
$originalResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//div[./div/div/span[contains(text(), "Original")]]'));
$originalResolutionButton->click();
sleep(2);
} catch (\Exception $e) {
Log::error("Failed to set original resolution: " . $e->getMessage());
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to set \"original\" resolution: " . $e->getMessage()));
}
}
protected function signin(Browser $browser)
{
if ($browser->assertSee("Search", true)) {
return; // Already signed in, skip some waiting for non existing text
}
try {
$browser->waitForText("Log in", 10, true);
sleep(3);
$emailButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "email")]'));
$emailButton->click();
$emailButton->sendKeys($this->jobInfos->get("instagram_repost_account_username"));
sleep(3);
$passwordButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "Password")]'));
$passwordButton->click();
$passwordButton->sendKeys($this->jobInfos->get("instagram_repost_account_password") . "\n");
sleep(5);
} catch (\Exception $e) {
// Probably no need to signin
}
try {
$browser->waitForText("Search", 15, true);
$this->removePopups($browser);
} catch (\Exception $e) {
Log::error("Failed to sign in: " . $e->getMessage());
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to sign in: " . $e->getMessage()));
// Stop the job run
throw $e;
}
}
protected function removePopups(Browser $browser)
{
$popupsTypes = [
['//button[contains(text(), "Allow all cookies")]'], // Allow all cookies
['//button[contains(text(), "Not Now")]', ["Popup Not Now clicked"]], // Not now
['//button[contains(text(), "OK")]', ["Popup Ok clicked"]], // OK
];
foreach ($popupsTypes as $popup) {
try {
$button = $browser->driver->findElement(WebDriverBy::xpath($popup[0]));
if ($button === null) {
continue; // No button found, continue to the next popup
}
if (isset($popup[1])) {
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, $popup[1][0]));
}
$button->click();
sleep(2);
return; // Exit after clicking the first popup found
} catch (\Exception $e) {
// Porbably no popup found, continue
}
}
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace App\Browser\Jobs\InstagramRepost;
use App\Browser\JobDebugScreenshot;
use App\Browser\Jobs\Instagram\InstagramAbstractJob;
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
use App\Models\InstagramNotification;
use App\Models\InstagramRepost;
use App\Models\Job;
use App\Models\JobRun;
use App\Notification\Notifications\JobDebugNotification;
use App\Notification\Providers\AllNotification;
use App\Services\Instagram\NotificationTypeDetector;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
use App\Services\AIPrompt\OpenAPIPrompt;
class InstagramNotificationHandlingJob extends InstagramAbstractJob implements ShouldBeUniqueUntilProcessing
{
// === CONFIGURATION ===
public $timeout = 1800; // 30 minutes
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
protected IInstagramVideoDownloader $videoDownloader;
protected ReelDescriptor $ReelDescriptor;
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
/**
* Pipeline for processing Instagram post descriptions.
* This pipeline can be used to modify the description before reposting.
* For example, it can remove account references or add hashtags.
*
* @var InstagramDescriptionPipeline
*/
protected InstagramDescriptionPipeline $descriptionPipeline;
public function __construct($jobId = 4, ReelDescriptor $ReelDescriptor = null, OpenAPIPrompt $openAPIPrompt = null)
{
parent::__construct($jobId, $openAPIPrompt);
$this->downloadFolder = base_path($this->downloadFolder);
$this->videoDownloader = new YTDLPDownloader();
$this->ReelDescriptor = $ReelDescriptor ?? app(ReelDescriptor::class);
$this->descriptionPipeline = new InstagramDescriptionPipeline([
// Add steps to the pipeline here
new DescriptionPipeline\RemoveAccountsReferenceStep(),
]);
}
public function run(Browser $browser): ?JobRun
{
$startTime = microtime(true);
Log::info("Running InstagramNotificationHandlingJob");
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
$this->jobRun = new JobRun([
"job_id" => $this->jobId,
"success" => false,
]);
$this->jobRun->save();
dump("visiting " . microtime(true) - $startTime);
$browser->visit('https://instagram.com');
sleep(5);
// dump("removing popups " . microtime(true) - $startTime);
// $this->removePopups($browser); // TEMPORARY DISABLED
dump("signing in " . microtime(true) - $startTime);
$this->signin($browser);
sleep(2);
dump("Saving notifications " . microtime(true) - $startTime);
$this->saveNotifications($browser);
sleep(seconds: 5);
$this->jobRun->success = true;
$this->jobRun->save();
Log::info("InstagramNotificationHandlingJob run ended");
return $this->jobRun;
}
/**
* @inheritDoc
*/
public function runTest(Browser $browser): ?JobRun
{
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
try {
$browser->visit('https://instagram.com');
sleep(2);
$this->removePopups($browser);
$this->signin($browser);
sleep(3);
return $this->makeSimpleJobRun(
true,
"Connexion réussie",
"Datboi a réussi à se connecter sur Instagram"
);
} catch (\Exception $e) {
return $this->makeSimpleJobRun(
false,
"Connexion échouée",
"Datboi n'a pas réussi à se connecter sur Instagram :\n" . $e->getMessage()
);
}
}
private function saveNotifications(Browser $browser): void
{
$this->openNotificationsPanel($browser);
try {
$DOMnotifications = $browser->driver->findElements(WebDriverBy::xpath('//div[@class="x6s0dn4 x1q4h3jn x78zum5 x1y1aw1k x64bnmy xwib8y2 x13jy36j x87ps6o x1wq6e7o x1ffbijf x1h4gsww xnp2e5m x1ypdohk x1l895ks"]')); // TODO : not rely on class names
$notifications = [];
foreach ($DOMnotifications as $DOMnotification) {
// Process each notification
$DOMnotificationText = $DOMnotification->findElement(WebDriverBy::xpath('./div[2]/span'));
$notification = new InstagramNotification();
$notification->username = str_replace( "/", "", $DOMnotificationText->findElement(WebDriverBy::xpath('./a[1]'))->getAttribute('href'));
$notification->notification_type = NotificationTypeDetector::detectType($DOMnotificationText->getText());
// 05/08/2025 : Instagram removed the link to the post in the notification, the day I'm working on it :()
$postId = null;
try {
$postId = str_replace(["\/p/", "/"], "", $DOMnotification->findElement(WebDriverBy::xpath('./div[3]/a'))->getAttribute('href'));
dump("Post ID: " . $postId);
$repostId = InstagramRepost::where('repost_reel_id', $postId)->first()?->id;
dump("Repost ID: " . $repostId);
if ($repostId) {
$notification->instagram_repost_id = $repostId;
}
}
catch (\Exception $e) {
Log::error("Failed to get post ID from notification : " . $e->getMessage());
}
$notification->message = $DOMnotificationText->getText();
// Save only if the notification is not already saved
try {
$notification->save();
} catch (\Exception $e) {
Log::error("Failed to save notification : " . $e->getMessage());
continue; // Skip to the next notification
}
dump("Notification saved : " . $notification);
$notifications[] = $notification;
}
} catch (\Exception $e) {
Log::error("Failed to get notifications : " . $e->getMessage());
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to open notifications panel: " . $e->getMessage()));
return;
}
}
private function openNotificationsPanel(Browser $browser): void
{
try {
$notificationsButtonXpath = '//a[./div//span[contains(text(), "Notifications")]]';
$notificationsButton = $browser->driver->findElement(WebDriverBy::xpath($notificationsButtonXpath));
// $notificationsButton->click();
$this->clickElementWithJavaScript($browser, $notificationsButtonXpath);
sleep(1);
$browser->waitForText('Filter', 10, true);
} catch (\Exception $e) {
Log::error("Failed to open notifications panel : " . $e->getMessage());
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to open notifications panel: " . $e->getMessage()));
return;
}
if (app()->environment('local')) {
Log::debug("Notifications panel opened successfully waiting 7 seconds for manual interaction");
sleep(7); // Allow time for manual interaction in local environment
}
}
}

View File

@ -2,8 +2,8 @@
namespace App\Browser\Jobs\InstagramRepost;
use App\Browser\BrowserJob;
use App\Browser\JobDebugScreenshot;
use App\Browser\Jobs\Instagram\InstagramAbstractJob;
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
use App\Models\InstagramRepost;
use App\Models\InstagramAccount;
@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
use App\Services\AIPrompt\OpenAPIPrompt;
class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
class InstagramRepostJob extends InstagramAbstractJob implements ShouldBeUniqueUntilProcessing
{
// === CONFIGURATION ===
@ -27,15 +27,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
private Collection $jobInfos;
protected JobRun $jobRun;
protected IInstagramVideoDownloader $videoDownloader;
protected ReelDescriptor $ReelDescriptor;
protected OpenAPIPrompt $openAPIPrompt;
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
/**
@ -49,12 +44,11 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
public function __construct($jobId = 4, ReelDescriptor $ReelDescriptor = null, OpenAPIPrompt $openAPIPrompt = null)
{
parent::__construct($jobId);
parent::__construct($jobId, $openAPIPrompt);
$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(),
@ -171,7 +165,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
$downloadInfos
];
$this->describeReel($repost, $downloadInfos);
if ($downloadInfos !== null) {
$this->describeReel($repost, $downloadInfos);
}
}
$this->jobRun->addArtifact(new JobArtifact([
@ -185,6 +182,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
$reel = $infos[0];
$videoInfo = $infos[1];
if ($videoInfo === null) {
continue; // Skip this reel if it failed to download
}
$repostSuccess = false;
do {
$repostSuccess = $this->repostReel($browser, $reel, $videoInfo);
@ -210,8 +211,12 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
}
}
private function getLatestReelsFromAccount(Browser $browser, string $account): array
private function getLatestReelsFromAccount(Browser $browser, string $account, int $maxReels = null, bool $save = true): array
{
if ($maxReels === null) {
$maxReels = config("jobs.instagramRepost.max_reposts_per_account");
}
$accountReels = []; // Indexed array to store new reels from the account
$browser->visit("https://instagram.com/{$account}/reels");
@ -251,12 +256,26 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
break;
}
$reelModel = InstagramRepost::firstOrCreate(
["reel_id" => $postId, "account_id" => $accountModel->id],
["reposted" => false, "repost_tries" => 0]
);
// Get the model for the reel
$query = InstagramRepost::where("reel_id", $postId)
->where("account_id", $accountModel->id)
->first();
if (count($accountReels) < config("jobs.instagramRepost.max_reposts_per_account")) {
if ($query) {
$reelModel = $query;
} else {
$reelModel = new InstagramRepost([
"reel_id" => $postId,
"account_id" => $accountModel->id,
"reposted" => false,
"repost_tries" => 0
]);
if ($save) {
$reelModel = $reelModel->save();
}
}
if (count($accountReels) < $maxReels) {
$accountReels[] = $reelModel; // Add it to the to be downloaded reels array
}
}
@ -360,6 +379,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
$reel->reposted = true;
$reel->save();
// set the repost ID to the lastest account reel ID
$reel->repost_reel_id = $this->getLatestPostId($browser, $reel);
$reel->save();
return true;
} catch (\Exception $e) {
@ -383,6 +406,25 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
}
}
private function getLatestPostId(Browser $browser, InstagramRepost $reel): void
{
// Go to the profile page of the managed account
$newReel = $this->getLatestReelsFromAccount(
$browser,
$this->jobInfos->get('instagram_repost_account_username'),
1,
false // Don't save the reel, we don't want to repost our own reposts
)[0] ?? null;
if ($newReel === null) {
Log::error("No reels found for account: " . $this->jobInfos->get('instagram_repost_account_username'));
return;
}
// Return the reel ID
return $newReel->reel_id;
}
private function getReelCaption(InstagramRepost $reel, IInstagramVideo $videoInfo): string
{
if (isset($reel->instagram_caption)) {
@ -395,32 +437,30 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
$llmAnswer = $this->openAPIPrompt->generate(
config('llm.models.chat.name'),
"Original Caption: {$originalDescription}
Video Description/Directive: {$reelDescription}",
llm_description: {$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.
systemMessage: "You are an expert Instagram caption writer. Your primary goal is to create short, engaging, concise captions for social media reels that capture the fun or relatability of the content without simply describing it like a transcript summary.
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.
Captions must:
1. Be brief and punchy.
2. Capture the essence or mood of the video.
3. Relate directly to the provided description (if available) or the core concept if no specific LLM description is given, but avoid copying phrasing awkwardly.
4. Encourage engagement relevant to the platform's algorithm (e.g., asking a question related to the joke/scene).
5. Optionally include relevant hashtags at the end (#hashtagsOnly), chosen appropriately for the reel's content or vibe. Use common tags if no specific ones are provided, but avoid overly generic ones unless fitting.
6. If credit information is provided in the input (e.g., `credit: twitteruser`), acknowledge it minimally within the caption text *using only that source*. Do not invent any account handles (`@`) or platform prefixes (`tt/`). Use phrases like \"Credited to...\" or simply insert the credited name if appropriate, but don't force it unless the core concept naturally includes attribution. If no credit is provided, do not mention a specific creator.
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 `#`.
**Do Not:**
* Start captions with 'This reel...' or similar intros.
* Describe the video content directly (replacing the LLM description role).
* Include platform-specific mentions (`tt/`, `@`) unless naturally part of the credited source's name format itself and used minimally as instructed for credit handling.
* Use overly complex sentences, slang that doesn't fit (#hashtags can be used), or long-winded explanations. Keep it to 1-3 short lines maximum.
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.
**Emojis:**
* Feel free to use emojis in moderation (e.g., 😂, 🤣, 😜, 👀) to add visual flair and emotion.
* They should enhance the caption but not be the *main* focus. Avoid excessive or random emojis that look unprofessional.
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.
",
Your response format must strictly adhere to JSON with only one required field: `answer`. Provide ONLY the generated caption string in this `answer` field, no explanations, markdown formatting, or other text.",
keepAlive: true,
shouldThink: config('llm.models.chat.shouldThink')
);
@ -432,80 +472,4 @@ Remember to be creative and ensure the generated caption feels like something yo
}
return $llmAnswer;
}
private function clickNext(Browser $browser) {
$nextButton = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(text(), "Next") or contains(text(), "Share")]'));
$nextButton->click();
sleep(2);
}
private function putOriginalResolution(Browser $browser)
{
try {
$chooseResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//button[./div/*[local-name() = "svg"][@aria-label="Select crop"]]'));
$chooseResolutionButton->click();
sleep(2);
// Choos "original" resolution
$originalResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//div[./div/div/span[contains(text(), "Original")]]'));
$originalResolutionButton->click();
sleep(2);
} catch (\Exception $e) {
Log::error("Failed to set original resolution: " . $e->getMessage());
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to set \"original\" resolution: " . $e->getMessage()));
}
}
protected function signin(Browser $browser)
{
try {
$browser->waitForText("Log in", 10, true);
sleep(3);
$emailButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "email")]'));
$emailButton->click();
$emailButton->sendKeys($this->jobInfos->get("instagram_repost_account_email"));
sleep(3);
$passwordButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "Password")]'));
$passwordButton->click();
$passwordButton->sendKeys($this->jobInfos->get("instagram_repost_account_password") . "\n");
sleep(5);
} catch (\Exception $e) {
// Probably no need to signin
}
try {
$browser->waitForText("Search", 15, true);
$this->removePopups($browser);
} catch (\Exception $e) {
Log::error("Failed to sign in: " . $e->getMessage());
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to sign in: " . $e->getMessage()));
}
}
protected function removePopups(Browser $browser)
{
$popupsTypes = [
['//button[contains(text(), "Allow all cookies")]'], // Allow all cookies
['//button[contains(text(), "Not Now")]', ["Popup Not Now clicked"]], // Not now
['//button[contains(text(), "OK")]', ["Popup Ok clicked"]], // OK
];
foreach ($popupsTypes as $popup) {
try {
$button = $browser->driver->findElement(WebDriverBy::xpath($popup[0]));
if ($button === null) {
continue; // No button found, continue to the next popup
}
if (isset($popup[1])) {
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, $popup[1][0]));
}
$button->click();
sleep(2);
return; // Exit after clicking the first popup found
} catch (\Exception $e) {
// Porbably no popup found, continue
}
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class InstagramNotification extends Model
{
protected $table = 'instagram_notifications';
protected $fillable = [
'username',
'instagram_repost_id',
'notification_type',
'message',
'is_read',
];
protected $casts = [
'notification_type' => InstagramNotificationType::class,
'is_read' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $attributes = [
'is_read' => false,
'is_processed' => false,
];
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
enum InstagramNotificationType: string
{
case LIKE = 'LIKE';
case COMMENT = 'COMMENT';
case FOLLOW = 'FOLLOW';
case MESSAGE = 'MESSAGE';
case SYSTEM = 'SYSTEM'; // System message like congratulatory messages or updates
case MENTION = 'MENTION';
case OTHER = 'OTHER'; // Uncategorized or unclassified notifications
}

View File

@ -4,6 +4,7 @@ namespace App\Providers;
use App\Services\AIPrompt\OpenAPIPrompt;
use App\Services\FileTools\OCR\IImageOCR;
use App\Services\FileTools\Transcription\IAudioTranscriptor;
use App\Services\FileTools\VideoDescriptor\IVideoDescriptor;
use Illuminate\Support\ServiceProvider;
@ -24,6 +25,11 @@ class VideoDescriptorServiceProvider extends ServiceProvider
// Register the VideoDescriptor service
$this->app->singleton(\App\Browser\Jobs\InstagramRepost\ReelDescriptor::class);
// Audio transcription service
$this->app->singleton(IAudioTranscriptor::class, function ($app) {
return new \App\Services\FileTools\Transcription\OpenAIAPIAudioTranscriptor();
});
}
/**

View File

@ -9,10 +9,11 @@ use Uri;
*/
class OpenAPIPrompt implements IAIPrompt
{
private string $host;
private ?string $host;
private ?string $token = null;
public function __construct(string $host = null) {
public function __construct(?string $host = null) {
//dd($host ?? config('llm.api.host')); // DEBUG TODO : Is null ? so thows error because $host is normally of type non null string
$this->host = $host ?? config('llm.api.host');
if (config('llm.api.token')) {
$this->token = config('llm.api.token');

View File

@ -9,7 +9,13 @@ class TesseractImageOCR implements IImageOCR
* @inheritDoc
*/
public function performOCR(string $filePath): string {
$tesseract = new TesseractOCR($filePath);
return $tesseract->run();
try {
$tesseract = new TesseractOCR($filePath);
return $tesseract->run();
} catch (\Exception $e) {
// Handle the exception, log it, or rethrow it as needed
// For now, we just return an empty string
return '';
}
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Services\FileTools\Transcription;
interface IAudioTranscriptor
{
/**
* Perform transcription on the given audio file.
*
* @param string $filePath The path to the audio file to be transcribed.
* @return string The transcribed text from the audio file.
*/
public function transcribe(string $filePath): ?string;
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Services\FileTools\Transcription;
use Log;
class OpenAIAPIAudioTranscriptor implements IAudioTranscriptor
{
private function getHeaders(): array
{
return [
'Authorization: ' . (config('llm.api.transcription.token') ? 'Bearer ' . config('llm.api.transcription.token') : ''),
//'Content-Type: application/json',
];
}
/**
* @inheritDoc
*/
public function transcribe(string $filePath): ?string
{
if (!file_exists($filePath)) {
Log::error("File not found: {$filePath}");
return null;
}
// Make a call to the API with curl
// Example of working curl command:
// curl -s "SPEACHES_BASE_URL/v1/audio/transcriptions" -F "file=@/home/ninluc/Downloads/memeSalto/m19.mp3" -F "model=MODEL_ID"
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => config('llm.api.transcription.host') . '/audio/transcriptions',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'file' => new \CURLFile($filePath),
'model' => config('llm.models.transcription.name'),
],
CURLOPT_HTTPHEADER => $this->getHeaders(),
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($httpCode !== 200) {
Log::error("Error during transcription: HTTP code {$httpCode} : {$response}");
return null;
}
$responseData = json_decode($response, true);
return $responseData['text'] ?? null;
}
}

View File

@ -56,4 +56,33 @@ abstract class AbstractLLMVideoDescriptor implements IVideoDescriptor
}
return $array;
}
/**
* Extract audio from the video file.
* Using ffmpeg to extract audio from the video file.
* The audio will be saved in a temporary directory as an MP3 file.
* If the audio extraction fails, it will return null.
* @param string $filePath
* @return string|null
*/
protected function extractAudioFromVideo(string $filePath): ?string
{
$tempDir = sys_get_temp_dir() . '/video_audio';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0777, true);
}
else {
// Clear the directory if it already exists
array_map('unlink', glob($tempDir . '/*'));
}
$outputFile = $tempDir . '/audio.mp3';
$command = "ffmpeg -i " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile);
exec($command);
if (file_exists($outputFile)) {
return $outputFile;
}
return null;
}
}

View File

@ -4,12 +4,14 @@ namespace App\Services\FileTools\VideoDescriptor;
use App\Services\AIPrompt\OpenAPIPrompt;
use App\Services\FileTools\OCR\IImageOCR;
use App\Services\FileTools\Transcription\IAudioTranscriptor;
class OCRLLMVideoDescriptor extends AbstractLLMVideoDescriptor implements IVideoDescriptor
{
public const DESCRIPTION_PROMPT = "Analyze this Video sequence. You are given information for each individual screenshot/analysis from the video:";
public function __construct(public IImageOCR $ocr, public OpenAPIPrompt $llm) {
public function __construct(public IImageOCR $ocr, public OpenAPIPrompt $llm, public IAudioTranscriptor $audioTranscriptor)
{
}
public function getDescription(string $filePath): ?string
@ -24,6 +26,14 @@ class OCRLLMVideoDescriptor extends AbstractLLMVideoDescriptor implements IVideo
// Step 1: Cut video into screenshots
$screenshots = $this->cutVideoIntoScreenshots($filePath);
$audio = $this->extractAudioFromVideo($filePath);
// Audio transcription
$audioTranscription = null;
if (isset($audio)) {
$audioTranscription = $this->audioTranscriptor->transcribe($audio);
dump($audioTranscription); // DEBUG
}
if (empty($screenshots)) {
throw new \Exception("No screenshots were generated from the video {$filePath}.");
@ -69,6 +79,17 @@ Please analyze the image carefully and provide a description focusing purely on
// Step 4: Combine the descriptions of all screenshots into a single description
$combinedDescription = '';
// Add full video informations
// Audio transcription
if (isset($audio)) {
$combinedDescription .= "Audio Transcription: {$audioTranscription}\n";
}
if (!empty($combinedDescription)) {
$combinedDescription .= "\n";
}
// Add screenshots descriptions
$screenshotCount = 0;
foreach ($screenshots as $values) {
$screenshot = $values['screenshot'];
@ -85,49 +106,41 @@ Please analyze the image carefully and provide a description focusing purely on
}
$combinedDescription = trim($combinedDescription);
dump($combinedDescription); // DEBUG
// Step 5: Ask an LLM to describe the video based on the combined descriptions
$llmDescription = $this->llm->generate(
config('llm.models.chat.name'),
static::DESCRIPTION_PROMPT . $combinedDescription . "\n\nBased only on these frame analyses, please provide:
A single, concise description that captures the main action or theme occurring in the reel across all frames.
Identify and describe any joke or humorous element present in the video if you can discern one.
Important Considerations
Remember that most videos are of poor quality; frame descriptions might be inaccurate, vague, or contradictory due to blurriness or fast cuts.
Your task is synthesis: focus on the overall impression and sequence, not perfecting each individual piece of information. Some details mentioned in one analysis may simply be incorrect or misidentified from another perspective.
Analyze all provided frames (separated by --- for clarity) to understand what's happening. Then, synthesize this understanding into point 1 above and identify the joke if present as per point 2.",
static::DESCRIPTION_PROMPT . $combinedDescription,
outputFormat: '{"type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"]}',
systemMessage: "You are an expert social media content analyst specializing in interpreting Instagram Reels. Your primary function is to generate a comprehensive description and identify any underlying humor or joke in a given video sequence. You will be provided with individual frame analyses, each containing:
systemMessage: "You are an expert social media content analyst specializing in Instagram Reels. Your task is to synthesize descriptions and OCR findings from multiple screenshots of a single video reel into a single, concise, and accurate overall description of the video's content, style, and potential humor.
Screenshot Number: The sequential number of the frame.
Timestamp: When that specific frame occurs within the reel.
OCR Text Result: Raw text extracted from the image content using OCR (Optical Character Recognition), which may contain errors or misinterpretations (\"may appear\" descriptions).
LLM Description of Screenshot: A textual interpretation of what's visible in the frame, based on previous LLM processing.
Your input will consist of:
1. An audio transcription of the entire video.
2. Multiple entries containing:
- Screenshot number (e.g., \"Screenshot: 1\")
- Timestamp (in seconds) indicating its position in the reel
- Raw OCR text from that specific screenshot, which may contain errors or unusual characters but should be interpreted for content relevance.
- A description of the image content generated by an LLM for that screenshot.
The descriptions provided by the LLM for individual screenshots are often inconsistent with adjacent frames and might not capture subtle humor accurately. The raw OCR text can sometimes provide direct quotes relevant to the context, even if misspelled or partially recognized.
Please note:
Your response must be in **exactly** the following JSON format:
```json
{
\"answer\": \"{your synthesized description here}\"
}
```
Please follow these instructions carefully:
The individual frame analyses can be inconsistent due to low video quality (e.g., blurriness) or rapid scene changes where details are hard to distinguish.
Your task is not to perfect each frame description but to understand the overall sequence and likely narrative, focusing on identifying any joke, irony, absurdity, or humorous transformation occurring across these frames.
Your response should be structured as follows:
Overall Video Description: Provide a concise summary of what happens in the reel based on the combined information from all the provided screenshots.
Humor/Joke Identification (If Applicable): If you can discern any joke or humorous element, explicitly state it and explain how the sequence of frames contributes to this.
Instructions for Synthesis:
Focus on identifying recurring elements, main subject(s), consistent actions/actions that seem unlikely (potential contradiction).
Look for patterns where details change rapidly or absurdly.
Prioritize information from descriptions over relying solely on OCR text if the description seems more plausible. Ignore minor inconsistencies between frames unless they clearly contradict a central theme or joke premise.
Be ready to point out where the humor lies, which might involve unexpected changes, wordplay captured by OCR errors in the context of the visual action described, absurdity, or irony.",
Analyze All Data: Consider both the audio transcription and all the screenshot data (OCR text and descriptions) together.
Synthesize Coherently: Create a single, flowing narrative that describes the main subject(s), actions, setting, transitions, sound/music, and overall style of the video reel based on the most consistent or contextually supported information across its frames.
Handle Inconsistencies: Assume that individual screenshot analyses might contain errors (especially with OCR) or be limited in scope. Do not rely solely on one frame's description contradicting another unless strongly supported by context and multiple data points converge to a different understanding or the inconsistency is clearly part of a joke requiring literal interpretation.
Focus on Repeated Elements: Pay close attention to subjects, actions, objects, text content (especially from OCR), sounds/words mentioned in the transcription, and visual styles that repeat across multiple frames, as this indicates continuity or recurring themes/humor.
Identify Joke/Humor: Actively look for elements within the combined data that suggest a joke, satire, absurdity, irony, sarcasm, clever wordplay (from OCR/transcription), or unexpected humor. This includes inconsistent descriptions if they are clearly intended as part of a gag, visual puns, audio-visual mismatches mentioned in the transcription, or any content designed for comedic effect.
Prioritize Core Content: Base your description primarily on the core subject and action within the reel (as identified repeatedly across frames). Use details from individual screenshots to flesh out specific moments only if they fit this narrative context.
Filter Minor Details: Ignore highly variable or insignificant details that appear inconsistent unless they are clearly integral to the joke or overall theme (e.g., slight variations in background color might be acceptable, but a consistent change is important).
Output Requirement: Your response must contain only valid JSON with an object having exactly one property answer of type string. Do not output any other text, explanations, lists, or code outside this JSON structure.",
keepAlive: true,
shouldThink: config('llm.models.chat.shouldThink')
);

View File

@ -0,0 +1,37 @@
<?php
namespace App\Services\Instagram;
use App\Models\InstagramNotificationType;
use Facebook\WebDriver\Remote\RemoteWebElement;
class NotificationTypeDetector
{
/**
* Detects the Instagram notification type from the DOM.
*
* @param string $descriptionText The text content of the notification description.
* @return InstagramNotificationType
*/
public static function detectType(string $descriptionText): InstagramNotificationType
{
$descriptionText = trim($descriptionText); // Remove leading/trailing whitespace
// Prioritize exact matches for reliability
if (strpos($descriptionText, 'liked') !== false) {
return InstagramNotificationType::LIKE;
} elseif (strpos($descriptionText, 'commented') !== false) {
return InstagramNotificationType::COMMENT;
} elseif (strpos($descriptionText, 'following you') !== false) {
return InstagramNotificationType::FOLLOW;
} elseif (strpos($descriptionText, 'message') !== false) {
return InstagramNotificationType::MESSAGE;
} elseif (strpos($descriptionText, 'congratulations') !== false || strpos($descriptionText, 'update') !== false) {
return InstagramNotificationType::SYSTEM;
} elseif (strpos($descriptionText, 'mention') !== false) {
return InstagramNotificationType::MENTION;
}
return InstagramNotificationType::OTHER;
}
}

View File

@ -39,8 +39,8 @@ services:
undetected-chromedriver:
build:
context: .
dockerfile: undetectedChromedriver/seleniumChromedriverDockerfile
context: ./undetectedChromedriver
dockerfile: seleniumChromedriverDockerfile
restart: unless-stopped
volumes:
- /tmp:/tmp

View File

@ -16,6 +16,20 @@ return [
* Null if not used
*/
'token' => env('LLM_API_TOKEN', null),
'transcription' => [
/**
* Host for the OpenAI transcription API.
* This should be the base URL of the OpenAI transcription API you are using with the API version (v1)
*/
'host' => env('TRANSC_API_HOST_URL', null),
/**
* Token for authenticating with the OpenAI transcription API.
* Null if not used
*/
'token' => env('TRANSC_API_TOKEN', null),
],
],
/**
@ -39,5 +53,9 @@ return [
'name' => env('LLM_VISION_MODEL', null),
'shouldThink' => env('LLM_VISION_MODEL_THINK', false),
],
'transcription' => [
'name' => env('TRANSC_TRANSCRIPTION_MODEL', null)
],
]
];

View File

@ -0,0 +1,50 @@
<?php
use App\Models\InstagramNotificationType;
use App\Models\InstagramRepost;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('instagram_notifications', function (Blueprint $table) {
$table->id();
$table->string('username')->nullable();
$table->foreignIdFor(InstagramRepost::class)->nullable()
->constrained('instagram_reposts')
->noActionOnDelete()
->cascadeOnUpdate();
$table->enum('notification_type', array_column(InstagramNotificationType::cases(), 'value'));
$table->text('message')->nullable();
$table->boolean('is_read')->default(false);
$table->boolean('is_processed')->default(false);
$table->unique(['username', 'instagram_repost_id', 'notification_type', 'message'], 'unique_instagram_notification');
$table->timestamps();
});
Schema::table('instagram_reposts', function (Blueprint $table) {
$table->string("repost_reel_id")->unique()->nullable()->comment('Reel ID of the reposted content');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('instagram_notifications');
Schema::table('instagram_reposts', function (Blueprint $table) {
$table->dropUnique(['repost_reel_id']);
$table->dropColumn("repost_reel_id");
});
}
};

View File

@ -0,0 +1,33 @@
<?php
use App\Models\JobInfo;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
JobInfo::where('key', 'instagram_repost_account_email')
->update([
'key' => 'instagram_repost_account_username',
'description' => "Le nom d'utilisateur unique utilisé pour le compte Instagram de repost."
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
JobInfo::where('key', 'instagram_repost_account_username')
->update([
'key' => 'instagram_repost_account_email',
'description' => "L'adresse e-mail/nom d'utilisateur/N° de téléphone utilisée pour le compte Instagram de repost."
]);
}
};

View File

@ -2,6 +2,7 @@
use App\Browser\Jobs\Hellcase\HellcaseJob;
use App\Browser\Jobs\HellcaseBattles\HellcaseBattlesJob;
use App\Browser\Jobs\InstagramRepost\InstagramNotificationHandlingJob;
use App\Browser\Jobs\InstagramRepost\InstagramRepostJob;
use App\Jobs\PruneOldJobRuns;
use App\Services\BrowserJobsInstances;
@ -24,4 +25,5 @@ Schedule::job(new PruneOldJobRuns)->monthly()->onOneServer()->withoutOverlapping
Schedule::job(new HellcaseJob)->daily()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
// Schedule::job(new HellcaseJob)->everyMinute()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
Schedule::job(new HellcaseBattlesJob)->hourly()->onOneServer()->withoutOverlapping()->name('hellcase_battles')->description('Hellcase battles job');
Schedule::job(new InstagramRepostJob())->everyThreeHours()->onOneServer()->withoutOverlapping()->name('instagram_reposts')->description('Intagrame reposts job');
Schedule::job(new InstagramRepostJob)->everyThreeHours()->onOneServer()->withoutOverlapping()->name('instagram_reposts')->description('Instagram reposts job');
Schedule::job(new InstagramNotificationHandlingJob)->hourly()->onOneServer()->withoutOverlapping()->name('instagram_reposts_notifications')->description('Instagram reposts notification handling job');

View File

@ -3,11 +3,11 @@
FROM selenium/standalone-chrome:latest AS final
COPY ./chromedriver /bin/chromedriver
#RUN mkdir -p /home/seluser/profile/
RUN mkdir -p /home/seluser/profile/nigga
ENV TZ=Europe/Brussels
# 15 minutes session timeout
ENV SE_OPTS="--session-timeout 900"
# 30 minutes session timeout
ENV SE_OPTS="--session-timeout 1800"
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -s http://localhost:4444/wd/hub/status | jq -e '.value.ready == true' || exit 1