diff --git a/app/Entities/Media/Transcript.php b/app/Entities/Media/Transcript.php index bbdb35aa..4221253c 100644 --- a/app/Entities/Media/Transcript.php +++ b/app/Entities/Media/Transcript.php @@ -15,12 +15,12 @@ use CodeIgniter\Files\File; class Transcript extends BaseMedia { + public ?string $json_path = null; + + public ?string $json_url = null; + protected string $type = 'transcript'; - protected ?string $json_path = null; - - protected ?string $json_url = null; - public function initFileProperties(): void { parent::initFileProperties(); diff --git a/app/Libraries/MediaClipper/VideoClipper.php b/app/Libraries/MediaClipper/VideoClipper.php index 6fd9b541..a8e3230b 100644 --- a/app/Libraries/MediaClipper/VideoClipper.php +++ b/app/Libraries/MediaClipper/VideoClipper.php @@ -11,10 +11,12 @@ declare(strict_types=1); namespace MediaClipper; use App\Entities\Episode; +use Exception; use GdImage; /** - * TODO: refactor this by splitting image manipulations + video generation + * TODO: refactor this by splitting process modules into different classes (image generation, subtitles clip, video + * generation) * * @phpstan-ignore-next-line */ @@ -45,8 +47,6 @@ class VideoClipper protected string $episodeCoverPath; - protected ?string $subtitlesInput = null; - protected string $soundbiteOutput; protected string $subtitlesClipOutput; @@ -87,9 +87,6 @@ class VideoClipper $this->audioInput = media_path($this->episode->audio->file_path); $this->episodeCoverPath = media_path($this->episode->cover->file_path); - if ($this->episode->transcript !== null) { - $this->subtitlesInput = media_path($this->episode->transcript->file_path); - } $podcastFolder = media_path("podcasts/{$this->episode->podcast->handle}"); @@ -108,12 +105,84 @@ class VideoClipper public function subtitlesClip(): void { - if ($this->subtitlesInput) { - $subtitleClipCmd = "ffmpeg -y -i {$this->subtitlesInput} -ss {$this->start} -t {$this->duration} {$this->subtitlesClipOutput}"; + if ($this->episode->transcript === null) { + throw new Exception('Episode does not have a transcript!'); + } + + if ($this->episode->transcript->json_path) { + $this->generateSubtitlesClipFromJson($this->episode->transcript->json_path); + } else { + $subtitlesInput = media_path($this->episode->transcript->file_path); + $subtitleClipCmd = "ffmpeg -y -i {$subtitlesInput} -ss {$this->start} -t {$this->duration} {$this->subtitlesClipOutput}"; exec($subtitleClipCmd); } } + public function generateSubtitlesClipFromJson(string $jsonFileInput): void + { + $jsonTranscriptString = file_get_contents($jsonFileInput); + if ($jsonTranscriptString === false) { + throw new Exception('Cannot get transcript json contents.'); + } + + $jsonTranscript = json_decode($jsonTranscriptString, true); + if ($jsonTranscript === null) { + throw new Exception('Transcript json is invalid.'); + } + + $srtClip = ''; + $segmentIndex = 1; + foreach ($jsonTranscript as $segment) { + $startTime = null; + $endTime = null; + + if ($segment['startTime'] < $this->end && $segment['endTime'] > $this->start) { + $startTime = $segment['startTime'] - $this->start; + $endTime = $segment['endTime'] - $this->start; + } + + if ($segment['startTime'] < $this->start && $this->start < $segment['endTime']) { + $startTime = 0; + } + + if ($segment['startTime'] < $this->end && $segment['endTime'] >= $this->end) { + $endTime = $this->duration; + } + + if ($startTime !== null && $endTime !== null) { + $formattedStartTime = $this->formatSeconds($startTime); + $formattedEndTime = $this->formatSeconds($endTime); + $srtClip .= << {$formattedEndTime} + {$segment['text']} + + + CODE_SAMPLE; + + ++$segmentIndex; + } + } + + // create srt clip file + file_put_contents($this->subtitlesClipOutput, $srtClip); + } + + public function formatSeconds(float $seconds): string + { + $milliseconds = str_replace('0.', '', (string) (round($seconds - floor($seconds), 3))); + + return gmdate('H:i:s', (int) floor($seconds)) . ',' . str_pad($milliseconds, 3, '0', STR_PAD_RIGHT); + } + + public function cleanTempFiles(): void + { + // delete generated video background image, soundbite & subtitlesClip + unlink($this->soundbiteOutput); + unlink($this->subtitlesClipOutput); + unlink($this->videoClipBgOutput); + } + /** * @return int 0 for success, else error */ @@ -129,7 +198,11 @@ class VideoClipper $generateCmd = $this->getCmd(); - return $this->cmd_exec($generateCmd); + $cmdResult = $this->cmd_exec($generateCmd); + + $this->cleanTempFiles(); + + return $cmdResult; } public function getCmd(): string diff --git a/app/Resources/js/modules/VideoClipBuilder.ts b/app/Resources/js/modules/VideoClipBuilder.ts index 11c1a637..432d1246 100644 --- a/app/Resources/js/modules/VideoClipBuilder.ts +++ b/app/Resources/js/modules/VideoClipBuilder.ts @@ -13,7 +13,7 @@ const VideoClipBuilder = (): void => { ) as NodeListOf; const titleInput = form.querySelector( - 'input[name="label"]' + 'input[name="title"]' ) as HTMLInputElement; if (titleInput) { videoClipPreviewer.setAttribute("title", titleInput.value || "");