diff --git a/.gitignore b/.gitignore
index 50cd1f20660fcd901d8b6153b8da5c2404e798a7..d0547825f3588c87a2200678069e04bf94bae24f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,6 +60,9 @@ writable/logs/*
 writable/session/*
 !writable/session/index.html
 
+writable/temp/*
+!writable/temp/index.html
+
 writable/uploads/*
 !writable/uploads/index.html
 
diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php
index f4274d6de256761f601f4dfc9e23fa3e43e6d661..a0df0da96865fd4cc3643dcc21cecc758d058906 100644
--- a/app/Libraries/MediaClipper/Config/MediaClipper.php
+++ b/app/Libraries/MediaClipper/Config/MediaClipper.php
@@ -229,8 +229,10 @@ class MediaClipper extends BaseConfig
      */
     public array $themes = [
         'pine' => [
-            // Preview must be a HSL colorscheme string
+            // Previews must be a HSL colorscheme string
             'preview' => '174 100% 29%',
+            'preview-background' => '172 100% 17%',
+            // arrays are rgb
             'background' => [0, 86, 74],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
@@ -248,6 +250,8 @@ class MediaClipper extends BaseConfig
         'crimson' => [
             // Preview must be a HSL colorscheme string
             'preview' => '350 87% 61%',
+            'preview-background' => '348 75% 40%',
+            // arrays are rgb
             'background' => [179, 31, 57],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
@@ -265,6 +269,8 @@ class MediaClipper extends BaseConfig
         'lake' => [
             // Preview must be a HSL colorscheme string
             'preview' => '194 100% 44%',
+            'preview-background' => '194 100% 22%',
+            // arrays are rgb
             'background' => [0, 86, 113],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
@@ -282,6 +288,8 @@ class MediaClipper extends BaseConfig
         'amber' => [
             // Preview must be a HSL colorscheme string
             'preview' => '17 100% 57%',
+            'preview-background' => '17 100% 35%',
+            // arrays are rgb
             'background' => [177, 50, 0],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
@@ -299,6 +307,8 @@ class MediaClipper extends BaseConfig
         'jacaranda' => [
             // Preview must be a HSL colorscheme string
             'preview' => '254 72% 52%',
+            'preview-background' => '254 73% 30%',
+            // arrays are rgb
             'background' => [47, 21, 132],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
@@ -316,6 +326,8 @@ class MediaClipper extends BaseConfig
         'onyx' => [
             // Preview must be a HSL colorscheme string
             'preview' => '240 17% 2%',
+            'preview-background' => '240 17% 2%',
+            // arrays are rgb
             'background' => [5, 5, 7],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
diff --git a/app/Libraries/MediaClipper/VideoClipper.php b/app/Libraries/MediaClipper/VideoClipper.php
index a8e3230ba1f127957fad31bb60af367d55e12d11..3e7cadb821daa7291707ce91c2aff78800cdfe69 100644
--- a/app/Libraries/MediaClipper/VideoClipper.php
+++ b/app/Libraries/MediaClipper/VideoClipper.php
@@ -55,6 +55,8 @@ class VideoClipper
 
     protected ?string $episodeNumbering = null;
 
+    protected string $tempFileOutput;
+
     /**
      * @var array<string, mixed>
      */
@@ -90,11 +92,22 @@ class VideoClipper
 
         $podcastFolder = media_path("podcasts/{$this->episode->podcast->handle}");
 
-        $this->soundbiteOutput = $podcastFolder . "/{$this->episode->slug}-soundbite-{$this->start}-to-{$this->end}.mp3";
-        $this->subtitlesClipOutput = $podcastFolder . "/{$this->episode->slug}-subtitles-clip-{$this->start}-to-{$this->end}.srt";
-        $this->videoClipBgOutput = $podcastFolder . "/{$this->episode->slug}-clip-bg-{$this->format}-{$this->theme}.png";
         $this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
         $this->videoClipFilePath = "podcasts/{$this->episode->podcast->handle}/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
+
+        // Temporary files to generate clip
+        $tempFile = tempnam(WRITEPATH . 'temp', "{$this->episode->slug}-soundbite-{$this->start}-to-{$this->end}");
+
+        if (! $tempFile) {
+            throw new Exception(
+                'Could not create temporary files, check for permissions on your ' . WRITEPATH . 'temp folder.'
+            );
+        }
+
+        $this->tempFileOutput = $tempFile;
+        $this->soundbiteOutput = $tempFile . '.mp3';
+        $this->subtitlesClipOutput = $tempFile . '.srt';
+        $this->videoClipBgOutput = $tempFile . '.png';
     }
 
     public function soundbite(): void
@@ -178,6 +191,7 @@ class VideoClipper
     public function cleanTempFiles(): void
     {
         // delete generated video background image, soundbite & subtitlesClip
+        unlink($this->tempFileOutput);
         unlink($this->soundbiteOutput);
         unlink($this->subtitlesClipOutput);
         unlink($this->videoClipBgOutput);
diff --git a/app/Models/ClipModel.php b/app/Models/ClipModel.php
index 1d9172b7df465f0728c90054ddccf3613e0dc70d..446e8ded498f7194bee6ea1cf2c9c04a476294b6 100644
--- a/app/Models/ClipModel.php
+++ b/app/Models/ClipModel.php
@@ -130,6 +130,20 @@ class ClipModel extends Model
         return $found;
     }
 
+    public function getRunningVideoClipsCount(): int
+    {
+        $result = $this
+            ->select('COUNT(*) as `running_count`')
+            ->where([
+                'type' => 'video',
+                'status' => 'running',
+            ])
+            ->get()
+            ->getResultArray();
+
+        return (int) $result[0]['running_count'];
+    }
+
     public function deleteVideoClip(int $podcastId, int $episodeId, int $clipId): BaseResult | bool
     {
         $this->clearVideoClipCache($clipId);
diff --git a/app/Resources/js/modules/VideoClipBuilder.ts b/app/Resources/js/modules/VideoClipBuilder.ts
index 432d12465d83a86aba737c612ec299f398095a98..b3a0865c67c0ac1ed09bd00fb079716f7df7dafa 100644
--- a/app/Resources/js/modules/VideoClipBuilder.ts
+++ b/app/Resources/js/modules/VideoClipBuilder.ts
@@ -36,15 +36,16 @@ const VideoClipBuilder = (): void => {
 
       let theme = form
         .querySelector('input[name="theme"]:checked')
-        ?.parentElement?.style.getPropertyValue("--color-accent-base");
+        ?.parentElement?.style.getPropertyValue("--color-background-preview");
       videoClipPreviewer.setAttribute("theme", theme || "");
 
       const watchThemeChange = (event: Event) => {
         theme =
           (
             event.target as HTMLInputElement
-          ).parentElement?.style.getPropertyValue("--color-accent-base") ??
-          theme;
+          ).parentElement?.style.getPropertyValue(
+            "--color-background-preview"
+          ) ?? theme;
         videoClipPreviewer.setAttribute("theme", theme || "");
       };
       for (let i = 0; i < themeOptions.length; i++) {
diff --git a/app/Resources/js/modules/video-clip-previewer.ts b/app/Resources/js/modules/video-clip-previewer.ts
index faa138af87553bb82d39c32ea7bb2c14cbc008ad..a28c2f2a57b2664ace7fbca992bc119aeda78fd2 100644
--- a/app/Resources/js/modules/video-clip-previewer.ts
+++ b/app/Resources/js/modules/video-clip-previewer.ts
@@ -31,7 +31,7 @@ export class VideoClipPreviewer extends LitElement {
   format: VideoFormats = VideoFormats.Portrait;
 
   @property()
-  theme = "173 44% 96%";
+  theme = "172 100% 17%";
 
   @property({ type: Number })
   duration!: number;
diff --git a/modules/Admin/Config/Admin.php b/modules/Admin/Config/Admin.php
index d71f39b738b2b2eb4728d897015bfa057ccadc5f..4c289d9cd936e175d150252499573ae820ca3a23 100644
--- a/modules/Admin/Config/Admin.php
+++ b/modules/Admin/Config/Admin.php
@@ -15,4 +15,10 @@ class Admin extends BaseConfig
      * Defines a base route for all admin pages
      */
     public string $gateway = 'cp-admin';
+
+    /**
+     * Number of maximum ffmpeg processes to spawn in parallel when generating video clips. Processes are instance wide,
+     * meaning that they are shared across all podcasts and episodes.
+     */
+    public int $videoClipWorkers = 2;
 }
diff --git a/modules/Admin/Controllers/SchedulerController.php b/modules/Admin/Controllers/SchedulerController.php
index a5f0239d98ad1ee0598e7e5af417dfcf41ff328a..2a00204d1850f2807ff173f477f33a50c64a2b17 100644
--- a/modules/Admin/Controllers/SchedulerController.php
+++ b/modules/Admin/Controllers/SchedulerController.php
@@ -13,12 +13,20 @@ namespace Modules\Admin\Controllers;
 use App\Models\ClipModel;
 use CodeIgniter\Controller;
 use CodeIgniter\I18n\Time;
+use Exception;
 use MediaClipper\VideoClipper;
 
 class SchedulerController extends Controller
 {
     public function generateVideoClips(): bool
     {
+        // get number of running clips to prevent from having too much running in parallel
+        // TODO: get the number of running ffmpeg processes directly from the machine?
+        $runningVideoClips = (new ClipModel())->getRunningVideoClipsCount();
+        if ($runningVideoClips >= config('Admin')->videoClipWorkers) {
+            return true;
+        }
+
         // get all clips that haven't been processed yet
         $scheduledClips = (new ClipModel())->getScheduledVideoClips();
 
@@ -38,40 +46,49 @@ class SchedulerController extends Controller
 
         // Loop through clips to generate them
         foreach ($scheduledClips as $scheduledClip) {
-            // set clip to pending
-            (new ClipModel())
-                ->update($scheduledClip->id, [
-                    'status' => 'running',
-                    'job_started_at' => Time::now(),
-                ]);
-            $clipper = new VideoClipper(
-                $scheduledClip->episode,
-                $scheduledClip->start_time,
-                $scheduledClip->end_time,
-                $scheduledClip->format,
-                $scheduledClip->theme['name'],
-            );
-            $exitCode = $clipper->generate();
+            try {
 
-            $clipModel = new ClipModel();
-            if ($exitCode === 0) {
-                // success, video was generated
-                $scheduledClip->setMedia($clipper->videoClipFilePath);
-                $clipModel->update($scheduledClip->id, [
-                    'media_id' => $scheduledClip->media_id,
-                    'status' => 'passed',
-                    'logs' => $clipper->logs,
-                    'job_ended_at' => Time::now(),
-                ]);
-            } else {
-                // error
-                $clipModel->update($scheduledClip->id, [
+                // set clip to pending
+                (new ClipModel())
+                    ->update($scheduledClip->id, [
+                        'status' => 'running',
+                        'job_started_at' => Time::now(),
+                    ]);
+                $clipper = new VideoClipper(
+                    $scheduledClip->episode,
+                    $scheduledClip->start_time,
+                    $scheduledClip->end_time,
+                    $scheduledClip->format,
+                    $scheduledClip->theme['name'],
+                );
+                $exitCode = $clipper->generate();
+
+                $clipModel = new ClipModel();
+                if ($exitCode === 0) {
+                    // success, video was generated
+                    $scheduledClip->setMedia($clipper->videoClipFilePath);
+                    $clipModel->update($scheduledClip->id, [
+                        'media_id' => $scheduledClip->media_id,
+                        'status' => 'passed',
+                        'logs' => $clipper->logs,
+                        'job_ended_at' => Time::now(),
+                    ]);
+                } else {
+                    // error
+                    $clipModel->update($scheduledClip->id, [
+                        'status' => 'failed',
+                        'logs' => $clipper->logs,
+                        'job_ended_at' => Time::now(),
+                    ]);
+                }
+                $clipModel->clearVideoClipCache($scheduledClip->id);
+            } catch (Exception $exception) {
+                (new ClipModel())->update($scheduledClip->id, [
                     'status' => 'failed',
-                    'logs' => $clipper->logs,
+                    'logs' => $exception,
                     'job_ended_at' => Time::now(),
                 ]);
             }
-            $clipModel->clearVideoClipCache($scheduledClip->id);
         }
 
         return true;
diff --git a/modules/Admin/Language/en/VideoClip.php b/modules/Admin/Language/en/VideoClip.php
index 8e804f2277202c28e6d8faf74b54cd862fda0450..84dfe89cd1cd7f55fa77ba92b5f2650cfa9571d9 100644
--- a/modules/Admin/Language/en/VideoClip.php
+++ b/modules/Admin/Language/en/VideoClip.php
@@ -38,17 +38,19 @@ return [
         'createSuccess' => 'Video clip has been successfully created!',
         'deleteSuccess' => 'Video clip has been successfully removed!',
     ],
+    'format' => [
+        'landscape' => 'Landscape',
+        'portrait' => 'Portrait',
+        'squared' => 'Squared',
+    ],
     'form' => [
         'title' => 'New video clip',
         'params_section_title' => 'Video clip parameters',
         'clip_title' => 'Clip title',
         'format' => [
             'label' => 'Choose a format',
-            'landscape' => 'Landscape',
             'landscape_hint' => 'With a 16:9 ratio, landscape videos are great for PeerTube, Youtube and Vimeo.',
-            'portrait' => 'Portrait',
             'portrait_hint' => 'With a 9:16 ratio, portrait videos are great for TikTok, Youtube shorts and Instagram stories.',
-            'squared' => 'Squared',
             'squared_hint' => 'With a 1:1 ratio, squared videos are great for Mastodon, Facebook, Twitter and LinkedIn.',
         ],
         'theme' => 'Select a theme',
diff --git a/modules/Admin/Language/fr/VideoClip.php b/modules/Admin/Language/fr/VideoClip.php
index a0646869ef81a1ca4e07655dbe9a5a92c7920867..c81eee5eebf315f2447d687e3dc02cdd5bb74928 100644
--- a/modules/Admin/Language/fr/VideoClip.php
+++ b/modules/Admin/Language/fr/VideoClip.php
@@ -38,17 +38,19 @@ return [
         'createSuccess' => 'L’extrait vidéo a été créé avec succès !',
         'deleteSuccess' => 'L’extrait vidéo a bien été supprimé !',
     ],
+    'format' => [
+        'landscape' => 'Paysage',
+        'portrait' => 'Portrait',
+        'squared' => 'Carré',
+    ],
     'form' => [
         'title' => 'Nouvel extrait vidéo',
         'params_section_title' => 'Paramètres de l’extrait vidéo',
         'clip_title' => 'Titre de l’extrait',
         'format' => [
             'label' => 'Choisissez un format',
-            'landscape' => 'Paysage',
             'landscape_hint' => 'Avec un ratio de 16/9, les vidéos en paysage sont adaptées pour PeerTube, Youtube et Vimeo.',
-            'portrait' => 'Portrait',
             'portrait_hint' => 'Avec un ratio de 9/16, les vidéos en portrait sont adaptées pour TikTok, les Youtube shorts and les stories Instagram.',
-            'squared' => 'Carré',
             'squared_hint' => 'Avec un ratio de 1/1,  les vidéos carrées sont adaptées pour Mastodon, Facebook, Twitter et LinkedIn.',
         ],
         'theme' => 'Sélectionnez un thème',
diff --git a/themes/cp_admin/episode/video_clips_list.php b/themes/cp_admin/episode/video_clips_list.php
index 6da9bc9d9f39b70137a76115c6d68793a3d38758..5af00a750be34c9d2b89240efad04036cbb53821 100644
--- a/themes/cp_admin/episode/video_clips_list.php
+++ b/themes/cp_admin/episode/video_clips_list.php
@@ -62,7 +62,7 @@ use CodeIgniter\I18n\Time;
                     'portrait' => 'aspect-[9/16]',
                     'squared' => 'aspect-square',
                 ];
-                return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . $videoClip->format . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '"><Icon glyph="play"/></span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' – <span class="font-semibold group-hover:underline">' . $videoClip->title . '</span><span class="ml-1 text-sm">by ' . $videoClip->user->username . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
+                return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . lang('VideoClip.format.' . $videoClip->format) . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '"><Icon glyph="play"/></span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' – <span class="font-semibold group-hover:underline">' . $videoClip->title . '</span><span class="ml-1 text-sm">by ' . $videoClip->user->username . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
             },
         ],
         [
diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php
index 1c9b3796a9d0dd7d36c34417aafba7a07ae94934..b8683f6efb6239d21d8643c94045975f92b15a48 100644
--- a/themes/cp_admin/episode/video_clips_new.php
+++ b/themes/cp_admin/episode/video_clips_new.php
@@ -39,17 +39,17 @@
                 name="format"
                 isChecked="true"
                 required="true"
-                hint="<?= lang('VideoClip.form.format.landscape_hint') ?>"><?= lang('VideoClip.form.format.landscape') ?></Forms.RadioButton>
+                hint="<?= lang('VideoClip.form.format.landscape_hint') ?>"><?= lang('VideoClip.format.landscape') ?></Forms.RadioButton>
             <Forms.RadioButton
                 value="portrait"
                 name="format"
                 required="true"
-                hint="<?= lang('VideoClip.form.format.portrait_hint') ?>"><?= lang('VideoClip.form.format.portrait') ?></Forms.RadioButton>
+                hint="<?= lang('VideoClip.form.format.portrait_hint') ?>"><?= lang('VideoClip.format.portrait') ?></Forms.RadioButton>
             <Forms.RadioButton
                 value="squared"
                 name="format"
                 required="true"
-                hint="<?= lang('VideoClip.form.format.squared_hint') ?>"><?= lang('VideoClip.form.format.squared') ?></Forms.RadioButton>
+                hint="<?= lang('VideoClip.form.format.squared_hint') ?>"><?= lang('VideoClip.format.squared') ?></Forms.RadioButton>
         </fieldset>
         <fieldset>
             <legend><?= lang('VideoClip.form.theme') ?></legend>
@@ -61,7 +61,7 @@
                     name="theme"
                     required="true"
                     isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
-                    style="--color-accent-base: <?= $colors['preview']?>"><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
+                    style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
                 <?php endforeach; ?>
             </div>
         </fieldset>
diff --git a/writable/temp/index.html b/writable/temp/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..eebf8ecb2b2bdf794e1a23e04bc129e3aaacaeb4
--- /dev/null
+++ b/writable/temp/index.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>403 Forbidden</title>
+  </head>
+  <body>
+    <p>Directory access is forbidden.</p>
+  </body>
+</html>