Update
This commit is contained in:
@@ -53,7 +53,7 @@ public static function getVectorEmbeddingBgeSmall($embedding_query)
|
||||
|
||||
KeywordEmbedding::create([
|
||||
'keyword' => $embedding_query,
|
||||
'embedding' => $embedding
|
||||
'embedding' => $embedding,
|
||||
]);
|
||||
|
||||
break;
|
||||
|
||||
@@ -102,7 +102,7 @@ public static function getSingleMemeGenerator($user_prompt)
|
||||
],
|
||||
'primary_keyword_type' => [
|
||||
'type' => 'string',
|
||||
'description' => "Primary keyword type, choose only between: (action|emotion|misc)",
|
||||
'description' => 'Primary keyword type, choose only between: (action|emotion|misc)',
|
||||
],
|
||||
'action_keywords' => [
|
||||
'type' => 'array',
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
use App\Models\MemeMedia;
|
||||
use App\Models\MemeMediaEmbedding;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Cloud;
|
||||
use PhpParser\Lexer\TokenEmulator\KeywordEmulator;
|
||||
|
||||
class KeywordEmbeddingMaintenance
|
||||
{
|
||||
@@ -24,7 +22,7 @@ public static function populateCategoryEmbeddings()
|
||||
|
||||
foreach ($categories as $category) {
|
||||
|
||||
$embedding_query = $category->name . " " . $category->description;
|
||||
$embedding_query = $category->name.' '.$category->description;
|
||||
|
||||
$keyword_embedding = KeywordEmbedding::where('keyword', $embedding_query)->first();
|
||||
|
||||
@@ -75,7 +73,6 @@ public static function populateMemeMediasKeywordsEmbeddings()
|
||||
|
||||
dump("{Processing: {$count}/{$max}: {$meme_media->name}");
|
||||
|
||||
|
||||
// keywords:
|
||||
foreach ($meme_media->keywords as $keyword) {
|
||||
|
||||
@@ -113,7 +110,6 @@ public static function populateMemeMediasKeywordsEmbeddings()
|
||||
dump("Populating emotion keyword embedding for {$keyword}");
|
||||
$embedding = self::fetchAndCacheEmbedding($keyword);
|
||||
|
||||
|
||||
if ($embedding) {
|
||||
MemeMediaEmbedding::create([
|
||||
'meme_media_id' => $meme_media->id,
|
||||
@@ -140,8 +136,8 @@ public static function populateMemeMediasKeywordsEmbeddings()
|
||||
}
|
||||
|
||||
dump("✓ Successfully processed meme media {$meme_media->id}");
|
||||
dump("");
|
||||
dump("");
|
||||
dump('');
|
||||
dump('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +146,7 @@ public static function populateMemeMediaEmbeddings()
|
||||
$meme_medias = MemeMedia::whereNotNull('embedding')->get();
|
||||
|
||||
foreach ($meme_medias as $meme_media) {
|
||||
$embedding_query = $meme_media->name . " " . $meme_media->description;
|
||||
$embedding_query = $meme_media->name.' '.$meme_media->description;
|
||||
|
||||
$keyword_embedding = KeywordEmbedding::where('keyword', $embedding_query)->first();
|
||||
|
||||
@@ -170,7 +166,6 @@ private static function fetchAndCacheEmbedding($keyword)
|
||||
$max_retries = 3;
|
||||
$current_attempt = 0;
|
||||
|
||||
|
||||
while ($embedding === null && $current_attempt < $max_retries) {
|
||||
$current_attempt++;
|
||||
try {
|
||||
|
||||
@@ -27,8 +27,6 @@ public static function generateMemesByCategories()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function patchMemeKeywords()
|
||||
{
|
||||
$meme_medias = MemeMedia::whereNull('action_keywords')->get();
|
||||
|
||||
@@ -27,7 +27,6 @@ public static function getSuitableMemeMedia(Meme $meme, $tolerance = 5)
|
||||
{
|
||||
$meme_media = null;
|
||||
|
||||
|
||||
$primary_keyword_type = $meme->primary_keyword_type;
|
||||
|
||||
if ($primary_keyword_type == 'action') {
|
||||
@@ -214,7 +213,6 @@ public static function getMemeMediaByKeywords(array $keywords, int $tolerance =
|
||||
|
||||
$meme_embedding = CloudflareAI::getVectorEmbeddingBgeSmall(implode(' ', $keywords));
|
||||
|
||||
|
||||
$meme_medias = MemeMediaEmbedding::query()
|
||||
->when(! is_empty($tag), function ($query) use ($tag) {
|
||||
return $query->where('tag', $tag);
|
||||
@@ -223,7 +221,6 @@ public static function getMemeMediaByKeywords(array $keywords, int $tolerance =
|
||||
->take($tolerance)
|
||||
->get();
|
||||
|
||||
|
||||
if ($meme_medias->count() > 0) {
|
||||
$meme_media = $meme_medias->random()->meme_media;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
* @property string|null $tag
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*
|
||||
* @package App\Models
|
||||
*/
|
||||
class KeywordEmbedding extends Model
|
||||
{
|
||||
@@ -33,6 +31,6 @@ class KeywordEmbedding extends Model
|
||||
protected $fillable = [
|
||||
'keyword',
|
||||
'embedding',
|
||||
'tag'
|
||||
'tag',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
* @property string|null $tag
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*
|
||||
* @property MemeMedia $meme_media
|
||||
*
|
||||
* @package App\Models
|
||||
*/
|
||||
class MemeMediaEmbedding extends Model
|
||||
{
|
||||
@@ -41,7 +38,7 @@ class MemeMediaEmbedding extends Model
|
||||
'meme_media_id',
|
||||
'keyword',
|
||||
'embedding',
|
||||
'tag'
|
||||
'tag',
|
||||
];
|
||||
|
||||
public function meme_media()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
BIN
public/fonts/Bungee/Bungee-Regular.ttf
Normal file
BIN
public/fonts/Bungee/Bungee-Regular.ttf
Normal file
Binary file not shown.
93
public/fonts/Bungee/OFL.txt
Normal file
93
public/fonts/Bungee/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2023 The Bungee Project Authors (https://github.com/djrrb/Bungee)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf
Normal file
BIN
public/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf
Normal file
BIN
public/fonts/Montserrat/Montserrat-VariableFont_wght.ttf
Normal file
Binary file not shown.
93
public/fonts/Montserrat/OFL.txt
Normal file
93
public/fonts/Montserrat/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2024 The Montserrat.Git Project Authors (https://github.com/JulietaUla/Montserrat.git)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
81
public/fonts/Montserrat/README.txt
Normal file
81
public/fonts/Montserrat/README.txt
Normal file
@@ -0,0 +1,81 @@
|
||||
Montserrat Variable Font
|
||||
========================
|
||||
|
||||
This download contains Montserrat as both variable fonts and static fonts.
|
||||
|
||||
Montserrat is a variable font with this axis:
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
Montserrat-VariableFont_wght.ttf
|
||||
Montserrat-Italic-VariableFont_wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Montserrat:
|
||||
static/Montserrat-Thin.ttf
|
||||
static/Montserrat-ExtraLight.ttf
|
||||
static/Montserrat-Light.ttf
|
||||
static/Montserrat-Regular.ttf
|
||||
static/Montserrat-Medium.ttf
|
||||
static/Montserrat-SemiBold.ttf
|
||||
static/Montserrat-Bold.ttf
|
||||
static/Montserrat-ExtraBold.ttf
|
||||
static/Montserrat-Black.ttf
|
||||
static/Montserrat-ThinItalic.ttf
|
||||
static/Montserrat-ExtraLightItalic.ttf
|
||||
static/Montserrat-LightItalic.ttf
|
||||
static/Montserrat-Italic.ttf
|
||||
static/Montserrat-MediumItalic.ttf
|
||||
static/Montserrat-SemiBoldItalic.ttf
|
||||
static/Montserrat-BoldItalic.ttf
|
||||
static/Montserrat-ExtraBoldItalic.ttf
|
||||
static/Montserrat-BlackItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them in your products & projects – print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
BIN
public/fonts/Montserrat/static/Montserrat-Black.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Black.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-BlackItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-Bold.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-BoldItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-ExtraBold.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-ExtraBoldItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-ExtraLight.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-ExtraLightItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-Italic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-Light.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-LightItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-LightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-Medium.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-MediumItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-Regular.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-SemiBold.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-SemiBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-SemiBoldItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-Thin.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-Thin.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Montserrat/static/Montserrat-ThinItalic.ttf
Normal file
BIN
public/fonts/Montserrat/static/Montserrat-ThinItalic.ttf
Normal file
Binary file not shown.
@@ -167,6 +167,7 @@ const Editor = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative mx-auto flex min-h-screen flex-col space-y-2 py-4" style={{ width: `${responsiveWidth}px` }}>
|
||||
<EditSidebar isOpen={isEditSidebarOpen} onClose={handleEditClose} />
|
||||
<EditNavSidebar isOpen={isEditNavSidebarOpen} onClose={handleEditNavClose} />
|
||||
@@ -211,6 +212,7 @@ const Editor = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
const VideoDownloadModal = ({ isOpen, onClose, ffmpegCommand, handleDownloadButton, isExporting, exportProgress, exportStatus }) => {
|
||||
const debug = true;
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Download Video</DialogTitle>
|
||||
{exportStatus ||
|
||||
(exportProgress > 0 && (
|
||||
<DialogDescription>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium">{exportStatus}</span>
|
||||
<span className="text-xs font-medium">{exportProgress}%</span>
|
||||
</div>
|
||||
<Spinner className={'h-5 w-5'} />
|
||||
</div>
|
||||
</DialogDescription>
|
||||
))}
|
||||
</DialogHeader>
|
||||
|
||||
{debug && <Textarea value={ffmpegCommand} readOnly />}
|
||||
|
||||
<Button onClick={handleDownloadButton}>{isExporting ? <Spinner className="text-secondary h-4 w-4" /> : 'Download'}</Button>
|
||||
|
||||
{/* Add your content here */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoDownloadModal;
|
||||
@@ -4,10 +4,13 @@ import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import SINGLE_CAPTION_TEMPLATE from '../../templates/single_caption_meme_background.json';
|
||||
import { generateTimelineFromTemplate } from '../../utils/timeline-template-processor';
|
||||
import VideoDownloadModal from './video-download/video-download-modal';
|
||||
import useVideoExport from './video-export';
|
||||
import VideoPreview from './video-preview';
|
||||
|
||||
const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
const [isDownloadModalOpen, setIsDownloadModalOpen] = useState(false);
|
||||
|
||||
const [showConsoleLogs] = useState(true);
|
||||
|
||||
const [dimensions] = useState({
|
||||
@@ -34,7 +37,7 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
const pausedTimeRef = useRef(0);
|
||||
|
||||
const { setVideoIsPlaying } = useVideoEditorStore();
|
||||
const { selectedMeme, selectedBackground, currentCaption } = useMediaStore();
|
||||
const { selectedMeme, selectedBackground, currentCaption, watermarked } = useMediaStore();
|
||||
|
||||
const FPS_INTERVAL = 1000 / 30; // 30 FPS
|
||||
|
||||
@@ -544,9 +547,19 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
});
|
||||
}, [handlePause, handleSeek, videoElements]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
exportVideo();
|
||||
}, [exportVideo]);
|
||||
|
||||
const handleOpenDownloadModal = () => {
|
||||
setIsDownloadModalOpen(true);
|
||||
};
|
||||
|
||||
const activeElements = getActiveElements(currentTime);
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on('video-open-download-modal', handleOpenDownloadModal);
|
||||
emitter.on('video-export', handleExport);
|
||||
emitter.on('video-play', handlePlay);
|
||||
emitter.on('video-reset', handleReset);
|
||||
emitter.on('video-seek', handleSeek);
|
||||
@@ -555,6 +568,8 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
emitter.off('video-open-download-modal', handleOpenDownloadModal);
|
||||
emitter.off('video-export', handleExport);
|
||||
emitter.off('video-play', handlePlay);
|
||||
emitter.off('video-reset', handleReset);
|
||||
emitter.off('video-seek', handleSeek);
|
||||
@@ -563,8 +578,10 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
}, [emitter, handlePlay, handleReset, handleSeek, handleElementUpdate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ width: dimensions.width, height: dimensions.height }} className="rounded-3xl">
|
||||
<VideoPreview
|
||||
watermarked={watermarked}
|
||||
dimensions={dimensions}
|
||||
currentTime={currentTime}
|
||||
totalDuration={totalDuration}
|
||||
@@ -590,6 +607,16 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
||||
layerRef={layerRef}
|
||||
/>
|
||||
</div>
|
||||
<VideoDownloadModal
|
||||
isOpen={isDownloadModalOpen}
|
||||
onClose={() => setIsDownloadModalOpen(false)}
|
||||
ffmpegCommand={ffmpegCommand}
|
||||
handleDownloadButton={handleExport}
|
||||
isExporting={isExporting}
|
||||
exportProgress={exportProgress}
|
||||
exportStatus={exportStatus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile, toBlobURL } from '@ffmpeg/util';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
// Font configuration mapping
|
||||
const FONT_CONFIG = {
|
||||
@@ -25,7 +25,7 @@ const FONT_CONFIG = {
|
||||
};
|
||||
|
||||
const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
const [showConsoleLogs] = useState(false);
|
||||
const [showConsoleLogs] = useState(true);
|
||||
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [exportProgress, setExportProgress] = useState(0);
|
||||
@@ -50,6 +50,10 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(JSON.stringify(timelineElements));
|
||||
}, [timelineElements]);
|
||||
|
||||
// Helper function to convert color format for FFmpeg
|
||||
const formatColorForFFmpeg = (color) => {
|
||||
// Handle hex colors (e.g., #ffffff or #fff)
|
||||
@@ -72,6 +76,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
const generateFFmpegCommand = useCallback(
|
||||
(is_string = true, useLocalFiles = false) => {
|
||||
showConsoleLogs && console.log('🎬 STARTING FFmpeg generation');
|
||||
showConsoleLogs && console.log(`📐 Canvas size: ${dimensions.width}x${dimensions.height}, Duration: ${totalDuration}s`);
|
||||
|
||||
const videos = timelineElements.filter((el) => el.type === 'video');
|
||||
const images = timelineElements.filter((el) => el.type === 'image');
|
||||
@@ -81,6 +86,20 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
showConsoleLogs && console.log('Images found:', images.length);
|
||||
showConsoleLogs && console.log('Texts found:', texts.length);
|
||||
|
||||
// Check for WebM videos with potential transparency
|
||||
const webmVideos = videos.filter((v) => v.source_webm && (v.source_webm.includes('.webm') || v.source_webm.includes('webm')));
|
||||
if (webmVideos.length > 0) {
|
||||
showConsoleLogs && console.log(`🌟 Found ${webmVideos.length} WebM video(s) - will preserve transparency`);
|
||||
}
|
||||
|
||||
// Summary of all elements for debugging
|
||||
showConsoleLogs && console.log('📋 Element Summary:');
|
||||
videos.forEach((v, i) => showConsoleLogs && console.log(` Video ${i}: Layer ${v.layer} (${v.x},${v.y}) ${v.width}x${v.height}`));
|
||||
images.forEach(
|
||||
(img, i) => showConsoleLogs && console.log(` Image ${i}: Layer ${img.layer} (${img.x},${img.y}) ${img.width}x${img.height}`),
|
||||
);
|
||||
texts.forEach((t, i) => showConsoleLogs && console.log(` Text ${i}: Layer ${t.layer} (${t.x},${t.y}) "${t.text.substring(0, 30)}..."`));
|
||||
|
||||
if (videos.length === 0 && images.length === 0) {
|
||||
if (is_string) {
|
||||
return 'ffmpeg -f lavfi -i color=black:size=450x800:duration=1 -c:v libx264 -t 1 output.mp4';
|
||||
@@ -110,32 +129,139 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
filters.push(`color=black:size=${dimensions.width}x${dimensions.height}:duration=${totalDuration}[base]`);
|
||||
|
||||
let videoLayer = 'base';
|
||||
let currentInputIndex = 0;
|
||||
|
||||
// Process video elements
|
||||
videos.forEach((v, i) => {
|
||||
filters.push(`[${currentInputIndex}:v]trim=start=${v.inPoint}:duration=${v.duration},setpts=PTS-STARTPTS[v${i}_trim]`);
|
||||
// FIXED: Sort all visual elements by layer, then process in layer order
|
||||
const allVisualElements = [
|
||||
...videos.map((v, i) => ({ ...v, elementType: 'video', originalIndex: i })),
|
||||
...images.map((img, i) => ({ ...img, elementType: 'image', originalIndex: i })),
|
||||
...texts.map((t, i) => ({ ...t, elementType: 'text', originalIndex: i })),
|
||||
].sort((a, b) => (a.layer || 0) - (b.layer || 0)); // Sort by layer (lowest first)
|
||||
|
||||
showConsoleLogs &&
|
||||
console.log(
|
||||
'🎭 Processing order by layer:',
|
||||
allVisualElements.map((el) => `${el.elementType}${el.originalIndex}(L${el.layer || 0})`).join(' → '),
|
||||
);
|
||||
|
||||
// Track input indices for videos and images
|
||||
let videoInputIndex = 0;
|
||||
let imageInputIndex = videos.length; // Images start after videos
|
||||
|
||||
// Process elements in layer order
|
||||
allVisualElements.forEach((element, processingIndex) => {
|
||||
if (element.elementType === 'video') {
|
||||
const v = element;
|
||||
const i = element.originalIndex;
|
||||
|
||||
showConsoleLogs &&
|
||||
console.log(
|
||||
`🎬 Video ${i} (Layer ${v.layer || 0}) - Position: (${v.x}, ${v.y}), Size: ${v.width}x${v.height}, Time: ${v.startTime}-${v.startTime + v.duration}`,
|
||||
);
|
||||
|
||||
// Check if video extends outside canvas
|
||||
if (v.x < 0 || v.y < 0 || v.x + v.width > dimensions.width || v.y + v.height > dimensions.height) {
|
||||
console.warn(`⚠️ Video ${i} extends outside canvas boundaries`);
|
||||
}
|
||||
|
||||
// Check if this is a WebM video (likely has transparency)
|
||||
const isWebM = v.source_webm && (v.source_webm.includes('.webm') || v.source_webm.includes('webm'));
|
||||
|
||||
filters.push(`[${videoInputIndex}:v]trim=start=${v.inPoint}:duration=${v.duration},setpts=PTS-STARTPTS[v${i}_trim]`);
|
||||
|
||||
// For WebM videos, preserve alpha channel during scaling
|
||||
if (isWebM) {
|
||||
showConsoleLogs && console.log(`🌟 Video ${i} is WebM - preserving alpha channel`);
|
||||
filters.push(`[v${i}_trim]scale=${Math.round(v.width)}:${Math.round(v.height)}:flags=bicubic[v${i}_scale]`);
|
||||
} else {
|
||||
filters.push(`[v${i}_trim]scale=${Math.round(v.width)}:${Math.round(v.height)}[v${i}_scale]`);
|
||||
}
|
||||
|
||||
// For overlay, ensure alpha blending is enabled for WebM
|
||||
if (isWebM) {
|
||||
filters.push(
|
||||
`[${videoLayer}][v${i}_scale]overlay=${Math.round(v.x)}:${Math.round(v.y)}:enable='between(t,${v.startTime},${
|
||||
v.startTime + v.duration
|
||||
})':format=auto:eof_action=pass[v${i}_out]`,
|
||||
);
|
||||
} else {
|
||||
filters.push(
|
||||
`[${videoLayer}][v${i}_scale]overlay=${Math.round(v.x)}:${Math.round(v.y)}:enable='between(t,${v.startTime},${
|
||||
v.startTime + v.duration
|
||||
})'[v${i}_out]`,
|
||||
);
|
||||
videoLayer = `v${i}_out`;
|
||||
currentInputIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
// Process image elements
|
||||
images.forEach((img, i) => {
|
||||
const imgInputIndex = currentInputIndex;
|
||||
filters.push(`[${imgInputIndex}:v]scale=${Math.round(img.width)}:${Math.round(img.height)}[img${i}_scale]`);
|
||||
videoLayer = `v${i}_out`;
|
||||
videoInputIndex++;
|
||||
} else if (element.elementType === 'image') {
|
||||
const img = element;
|
||||
const i = element.originalIndex;
|
||||
|
||||
showConsoleLogs &&
|
||||
console.log(
|
||||
`🖼️ Image ${i} (Layer ${img.layer || 0}) - Position: (${img.x}, ${img.y}), Size: ${img.width}x${img.height}, Time: ${img.startTime}-${img.startTime + img.duration}`,
|
||||
);
|
||||
|
||||
// Check if image is larger than canvas or positioned outside
|
||||
if (img.width > dimensions.width || img.height > dimensions.height) {
|
||||
console.warn(`⚠️ Image ${i} (${img.width}x${img.height}) is larger than canvas (${dimensions.width}x${dimensions.height})`);
|
||||
}
|
||||
if (img.x < 0 || img.y < 0 || img.x + img.width > dimensions.width || img.y + img.height > dimensions.height) {
|
||||
console.warn(`⚠️ Image ${i} extends outside canvas boundaries`);
|
||||
}
|
||||
|
||||
filters.push(`[${imageInputIndex}:v]scale=${Math.round(img.width)}:${Math.round(img.height)}[img${i}_scale]`);
|
||||
filters.push(
|
||||
`[${videoLayer}][img${i}_scale]overlay=${Math.round(img.x)}:${Math.round(img.y)}:enable='between(t,${img.startTime},${
|
||||
img.startTime + img.duration
|
||||
})'[img${i}_out]`,
|
||||
);
|
||||
videoLayer = `img${i}_out`;
|
||||
currentInputIndex++;
|
||||
imageInputIndex++;
|
||||
} else if (element.elementType === 'text') {
|
||||
const t = element;
|
||||
const i = element.originalIndex;
|
||||
|
||||
showConsoleLogs &&
|
||||
console.log(`📝 Text ${i} (Layer ${t.layer || 0}) - Position: (${t.x}, ${t.y}) Text: "${t.text.substring(0, 30)}..."`);
|
||||
|
||||
// Better text escaping for FFmpeg
|
||||
const escapedText = t.text
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||
.replace(/'/g, is_string ? "\\'" : "'") // Escape quotes
|
||||
.replace(/:/g, '\\:') // Escape colons
|
||||
.replace(/\[/g, '\\[') // Escape square brackets
|
||||
.replace(/\]/g, '\\]')
|
||||
.replace(/,/g, '\\,') // Escape commas
|
||||
.replace(/;/g, '\\;'); // Escape semicolons
|
||||
|
||||
// Get the appropriate font file path
|
||||
const fontFilePath = getFontFilePath(t.fontFamily, t.fontWeight, t.fontStyle);
|
||||
const fontFileName = fontFilePath.split('/').pop();
|
||||
|
||||
// Center the text: x position is the center point, y is adjusted for baseline
|
||||
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
|
||||
|
||||
// Format colors for FFmpeg
|
||||
const fontColor = formatColorForFFmpeg(t.fill);
|
||||
const borderColor = formatColorForFFmpeg(t.stroke);
|
||||
const borderWidth = Math.max(0, t.strokeWidth || 0); // Ensure non-negative
|
||||
|
||||
// Build drawtext filter with proper border handling
|
||||
// For centering: use (w-tw)/2 for x and adjust y as needed
|
||||
let drawTextFilter = `[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=(w-tw)/2:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${fontColor}`;
|
||||
|
||||
// Only add border if strokeWidth > 0
|
||||
if (borderWidth > 0) {
|
||||
drawTextFilter += `:borderw=${borderWidth}:bordercolor=${borderColor}`;
|
||||
}
|
||||
|
||||
drawTextFilter += `:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`;
|
||||
|
||||
showConsoleLogs && console.log(`Text filter ${i}:`, drawTextFilter);
|
||||
filters.push(drawTextFilter);
|
||||
videoLayer = `t${i}`;
|
||||
}
|
||||
});
|
||||
|
||||
showConsoleLogs && console.log('🎵 PROCESSING AUDIO FOR', videos.length, 'VIDEOS');
|
||||
@@ -159,39 +285,14 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
||||
|
||||
// Process text elements with proper font support and color handling
|
||||
texts.forEach((t, i) => {
|
||||
const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:');
|
||||
|
||||
// Get the appropriate font file path
|
||||
const fontFilePath = getFontFilePath(t.fontFamily, t.fontWeight, t.fontStyle);
|
||||
const fontFileName = fontFilePath.split('/').pop();
|
||||
|
||||
// Center the text: x position is the center point, y is adjusted for baseline
|
||||
const centerX = Math.round(t.x);
|
||||
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
|
||||
|
||||
// Format colors for FFmpeg
|
||||
const fontColor = formatColorForFFmpeg(t.fill);
|
||||
const borderColor = formatColorForFFmpeg(t.stroke);
|
||||
const borderWidth = Math.max(0, t.strokeWidth || 0); // Ensure non-negative
|
||||
|
||||
// Build drawtext filter with proper border handling
|
||||
let drawTextFilter = `[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${fontColor}`;
|
||||
|
||||
// Only add border if strokeWidth > 0
|
||||
if (borderWidth > 0) {
|
||||
drawTextFilter += `:borderw=${borderWidth}:bordercolor=${borderColor}`;
|
||||
}
|
||||
|
||||
drawTextFilter += `:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`;
|
||||
|
||||
filters.push(drawTextFilter);
|
||||
videoLayer = `t${i}`;
|
||||
});
|
||||
|
||||
const filterComplex = filters.join('; ');
|
||||
showConsoleLogs && console.log('🎵 Filter includes atrim:', filterComplex.includes('atrim'));
|
||||
showConsoleLogs && console.log('📝 Complete filter complex:', filterComplex);
|
||||
showConsoleLogs &&
|
||||
console.log(
|
||||
`🎭 Final layer order:`,
|
||||
allVisualElements.map((el) => `${el.elementType}${el.originalIndex}(L${el.layer || 0})`).join(' → '),
|
||||
);
|
||||
|
||||
const finalArgs = [
|
||||
...inputArgs,
|
||||
@@ -263,7 +364,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
});
|
||||
|
||||
ffmpeg.on('log', ({ message }) => {
|
||||
showConsoleLogs && console.log(message);
|
||||
showConsoleLogs && console.log('FFmpeg Log:', message);
|
||||
});
|
||||
|
||||
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
||||
@@ -288,44 +389,43 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
setExportStatus('Loading fonts...');
|
||||
|
||||
// Load all required fonts
|
||||
const fontsToLoad = new Set();
|
||||
// Collect all fonts that need to be loaded with their correct paths
|
||||
const fontsToLoad = new Map(); // Map from filename to full path
|
||||
|
||||
// Add Arial font (fallback)
|
||||
fontsToLoad.add('arial.ttf');
|
||||
fontsToLoad.set('arial.ttf', 'https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf');
|
||||
|
||||
// Add fonts used by text elements
|
||||
// Add fonts used by text elements - FIXED: use the actual font paths from getFontFilePath
|
||||
timelineElements
|
||||
.filter((el) => el.type === 'text')
|
||||
.forEach((text) => {
|
||||
const fontFilePath = getFontFilePath(text.fontFamily, text.fontWeight, text.fontStyle);
|
||||
const fontFileName = fontFilePath.split('/').pop();
|
||||
fontsToLoad.add(fontFileName);
|
||||
|
||||
// Only add if not already in map and not arial.ttf
|
||||
if (fontFileName !== 'arial.ttf' && !fontsToLoad.has(fontFileName)) {
|
||||
fontsToLoad.set(fontFileName, fontFilePath); // Use the actual path, not reconstructed
|
||||
}
|
||||
});
|
||||
|
||||
showConsoleLogs && console.log('Fonts to load:', Array.from(fontsToLoad.entries()));
|
||||
|
||||
// Load each unique font
|
||||
let fontProgress = 0;
|
||||
for (const fontFile of fontsToLoad) {
|
||||
for (const [fontFileName, fontPath] of fontsToLoad) {
|
||||
try {
|
||||
if (fontFile === 'arial.ttf') {
|
||||
await ffmpeg.writeFile(
|
||||
'arial.ttf',
|
||||
await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'),
|
||||
);
|
||||
} else {
|
||||
// Load Montserrat fonts from local filesystem
|
||||
const fontPath = `/fonts/Montserrat/static/${fontFile}`;
|
||||
await ffmpeg.writeFile(fontFile, await fetchFile(fontPath));
|
||||
}
|
||||
showConsoleLogs && console.log(`Loading font: ${fontFileName} from ${fontPath}`);
|
||||
await ffmpeg.writeFile(fontFileName, await fetchFile(fontPath));
|
||||
showConsoleLogs && console.log(`✓ Font ${fontFileName} loaded successfully`);
|
||||
fontProgress++;
|
||||
setExportProgress(10 + Math.round((fontProgress / fontsToLoad.size) * 10));
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load font ${fontFile}, falling back to arial.ttf:`, error);
|
||||
console.error(`❌ Failed to load font ${fontFileName} from ${fontPath}:`, error);
|
||||
// If font loading fails, we'll use arial.ttf as fallback
|
||||
}
|
||||
}
|
||||
|
||||
showConsoleLogs && console.log('Fonts loaded!');
|
||||
showConsoleLogs && console.log('All fonts loaded!');
|
||||
setExportProgress(20);
|
||||
|
||||
setExportStatus('Downloading media...');
|
||||
@@ -333,27 +433,69 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
const images = timelineElements.filter((el) => el.type === 'image');
|
||||
const totalMedia = videos.length + images.length;
|
||||
|
||||
showConsoleLogs && console.log(`Total media to download: ${totalMedia} (${videos.length} videos, ${images.length} images)`);
|
||||
|
||||
let mediaProgress = 0;
|
||||
|
||||
// Download videos
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
try {
|
||||
showConsoleLogs && console.log(`Downloading video ${i}: ${videos[i].source_webm}`);
|
||||
await ffmpeg.writeFile(`input_video_${i}.webm`, await fetchFile(videos[i].source_webm));
|
||||
showConsoleLogs && console.log(`✓ Video ${i} downloaded`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to download video ${i}:`, error);
|
||||
throw new Error(`Failed to download video ${i}: ${error.message}`);
|
||||
}
|
||||
mediaProgress++;
|
||||
setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40));
|
||||
}
|
||||
|
||||
// Download images
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
try {
|
||||
showConsoleLogs && console.log(`Downloading image ${i}: ${images[i].source}`);
|
||||
await ffmpeg.writeFile(`input_image_${i}.jpg`, await fetchFile(images[i].source));
|
||||
showConsoleLogs && console.log(`✓ Image ${i} downloaded`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to download image ${i}:`, error);
|
||||
throw new Error(`Failed to download image ${i}: ${error.message}`);
|
||||
}
|
||||
mediaProgress++;
|
||||
setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40));
|
||||
}
|
||||
|
||||
showConsoleLogs && console.log('All media downloaded successfully!');
|
||||
|
||||
// List all files in FFmpeg filesystem for debugging
|
||||
try {
|
||||
const files = await ffmpeg.listDir('/');
|
||||
showConsoleLogs && console.log('Files in FFmpeg filesystem:', files);
|
||||
} catch (listError) {
|
||||
console.warn('Could not list FFmpeg filesystem:', listError);
|
||||
}
|
||||
|
||||
setExportStatus('Processing video...');
|
||||
let args = generateFFmpegCommand(false, true);
|
||||
|
||||
showConsoleLogs && console.log('Generated FFmpeg arguments:', args);
|
||||
|
||||
setExportProgress(70);
|
||||
|
||||
try {
|
||||
await ffmpeg.exec(args);
|
||||
showConsoleLogs && console.log('FFmpeg execution completed successfully!');
|
||||
} catch (execError) {
|
||||
console.error('FFmpeg execution failed:', execError);
|
||||
console.error('Failed arguments:', args);
|
||||
|
||||
// Log the specific error details
|
||||
if (execError.message) {
|
||||
console.error('Error message:', execError.message);
|
||||
}
|
||||
|
||||
throw new Error(`FFmpeg execution failed: ${execError.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
setExportStatus('Downloading...');
|
||||
setExportProgress(90);
|
||||
@@ -375,7 +517,15 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
ffmpeg.terminate();
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
console.error('Full export error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
code: error.code,
|
||||
errno: error.errno,
|
||||
path: error.path,
|
||||
error: error,
|
||||
});
|
||||
setExportStatus(`Failed: ${error.message}`);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useElementTransform } from './video-preview/video-preview-element-trans
|
||||
import { getImageSource, getTextFontStyle } from './video-preview/video-preview-utils';
|
||||
|
||||
const VideoPreview = ({
|
||||
watermarked,
|
||||
// Dimensions
|
||||
dimensions,
|
||||
|
||||
@@ -242,6 +243,27 @@ const VideoPreview = ({
|
||||
return null;
|
||||
})}
|
||||
|
||||
{/* Watermark - only show when watermarked is true */}
|
||||
{watermarked && (
|
||||
<Text
|
||||
text="MEMEAIGEN.COM"
|
||||
x={dimensions.width / 2}
|
||||
y={dimensions.height / 2 + dimensions.height * 0.2}
|
||||
fontSize={20}
|
||||
fontFamily="Bungee"
|
||||
fill="white"
|
||||
stroke="black"
|
||||
strokeWidth={2}
|
||||
opacity={0.5}
|
||||
align="center"
|
||||
verticalAlign="middle"
|
||||
offsetX={90} // Approximate half-width to center the text
|
||||
offsetY={5} // Approximate half-height to center the text
|
||||
draggable={false}
|
||||
listening={false} // Prevents any mouse interactions
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Guide Lines Layer */}
|
||||
{guideLines.showVertical && (
|
||||
<Line
|
||||
|
||||
@@ -18,6 +18,10 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
|
||||
emitter.emit('video-reset');
|
||||
};
|
||||
|
||||
const handleDownloadButton = () => {
|
||||
emitter.emit('video-open-download-modal');
|
||||
};
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (videoIsPlaying) {
|
||||
handleReset();
|
||||
@@ -50,7 +54,7 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
|
||||
<Edit3 className={`h-8 w-8 ${isEditActive ? 'text-white' : ''}`} />
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Button onClick={handleDownloadButton} variant="outline" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Download className="h-8 w-8" />
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -23,4 +23,15 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, 'resources/js'),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util']
|
||||
},
|
||||
|
||||
server: {
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user