160 Commits

Author SHA1 Message Date
Aria 8e0be4695c Añadir src/pages/index.tsx 2026-06-25 01:08:10 +00:00
Aria 63098d4c45 Eliminar src/pages/index.tsx 2026-06-25 01:07:51 +00:00
Aria 6c75222aad Añadir src/pages/index.tsx 2026-06-25 00:51:27 +00:00
Aria 0d6662030b Eliminar src/pages/index.tsx 2026-06-25 00:50:00 +00:00
Aria 4d869511c5 Añadir src/pages/index.tsx 2026-06-23 22:22:38 +00:00
Aria 1004993567 Eliminar src/pages/index.tsx 2026-06-23 22:21:51 +00:00
Aria 2385790755 Añadir src/pages/index.tsx 2026-06-23 21:53:25 +00:00
Aria ea1bc0ef66 Eliminar src/pages/index.tsx 2026-06-23 21:53:06 +00:00
Aria c26f1641da Añadir src/pages/index.tsx 2026-06-23 21:36:14 +00:00
Aria 8975c5c07f Eliminar src/pages/index.tsx 2026-06-23 21:29:25 +00:00
Aria 3cecfbecf3 Añadir src/pages/index.tsx 2026-06-21 23:26:46 +00:00
Aria e77d251785 Eliminar src/pages/index.tsx 2026-06-21 23:26:11 +00:00
Aria 9f4b6d2312 Añadir src/pages/index.tsx 2026-06-21 23:21:49 +00:00
Aria 12aa29fb58 Eliminar src/pages/index.tsx 2026-06-21 23:21:11 +00:00
Aria 5e2d996422 Añadir src/pages/api/render.ts 2026-06-21 23:13:53 +00:00
Aria de2eb40709 Eliminar src/pages/api/render.ts 2026-06-21 23:13:31 +00:00
Aria ac1ab97cbe Añadir src/pages/index.tsx 2026-06-21 23:09:47 +00:00
Aria 30310151d1 Eliminar src/pages/index.tsx 2026-06-21 23:09:16 +00:00
Aria f083af7303 Añadir src/pages/index.tsx 2026-06-21 22:12:51 +00:00
Aria 3f86278f6d Eliminar src/pages/index.tsx 2026-06-21 22:12:26 +00:00
Aria b3a776aa7d Añadir src/pages/index.tsx 2026-06-21 22:10:14 +00:00
Aria 58b24b28dd Eliminar src/pages/index.tsx 2026-06-21 22:08:05 +00:00
Aria 45f2bba1a0 Añadir src/pages/index.tsx 2026-06-21 22:07:52 +00:00
Aria ab340263ab Eliminar src/pages/index.tsx 2026-06-21 22:00:33 +00:00
Aria 12798e05d9 Añadir src/pages/api/render.ts 2026-06-21 21:59:30 +00:00
Aria 39acebd01a Añadir src/pages/index.tsx 2026-06-21 21:08:16 +00:00
Aria fbf87ea175 Eliminar src/pages/index.tsx 2026-06-21 21:07:41 +00:00
Aria 38068322d4 Añadir src/pages/index.tsx 2026-06-21 20:43:24 +00:00
Aria bb5adf4513 Eliminar src/pages/index.tsx 2026-06-21 20:42:57 +00:00
Aria 0feb2f0fe9 Añadir src/pages/index.tsx 2026-06-21 20:30:14 +00:00
Aria 39f81b588e Eliminar src/pages/index.tsx 2026-06-21 20:28:55 +00:00
Aria 1a519f89aa Añadir src/pages/index.tsx 2026-06-21 20:15:00 +00:00
Aria eab336d9c6 Eliminar src/pages/index.tsx 2026-06-21 20:14:35 +00:00
Aria b2c8e502a9 Añadir src/pages/index.tsx 2026-06-21 19:47:07 +00:00
Aria 75cf83edd0 Eliminar src/pages/index.tsx 2026-06-21 19:46:45 +00:00
Aria d737c7d0e9 Añadir src/pages/index.tsx 2026-06-21 19:08:15 +00:00
Aria bbe288bd74 Eliminar src/pages/index.tsx 2026-06-21 19:07:55 +00:00
Aria a31a1ea6fd Añadir src/pages/index.tsx 2026-06-21 18:52:57 +00:00
Aria ab535d4f1f Eliminar src/pages/index.tsx 2026-06-21 18:52:31 +00:00
Aria fe5a7dfc28 Añadir src/pages/index.tsx 2026-06-21 16:49:14 +00:00
Aria 98df2b864a Eliminar src/pages/index.tsx 2026-06-21 16:48:20 +00:00
Aria 085612e549 Añadir src/pages/index.tsx 2026-06-21 16:30:18 +00:00
Aria 2b11f55be1 Eliminar src/pages/index.tsx 2026-06-21 16:29:57 +00:00
Aria a4e41b15f1 Añadir src/pages/index.tsx 2026-06-21 16:20:01 +00:00
Aria 6ea3c5631c Eliminar src/pages/index.tsx 2026-06-21 16:19:39 +00:00
Aria ffa1a7fa51 Añadir src/pages/api/clean-video.ts 2026-06-21 15:21:57 +00:00
Aria 009af13ecf Eliminar src/pages/api/clean-video.ts 2026-06-21 15:21:32 +00:00
Aria 6c95a18ab2 Añadir src/pages/api/clean-video.ts 2026-06-21 15:02:22 +00:00
Aria 864622f346 Eliminar src/pages/api/clean-video.ts 2026-06-21 15:01:59 +00:00
Aria 9ce93a5321 Añadir src/pages/index.tsx 2026-06-21 15:00:04 +00:00
Aria a82109d82b Eliminar src/pages/index.tsx 2026-06-21 14:59:35 +00:00
Aria cdc8b9f1c0 Añadir src/pages/index.tsx 2026-06-21 11:32:57 +00:00
Aria fbfc8eaefc Eliminar src/pages/index.tsx 2026-06-21 11:32:33 +00:00
Aria 0fb2542571 Añadir src/pages/index.tsx 2026-06-21 10:58:24 +00:00
Aria 35f0ceb105 Eliminar src/pages/index.tsx 2026-06-21 10:57:11 +00:00
Aria 80c85626b8 Añadir Dockerfile 2026-06-21 10:38:57 +00:00
Aria 82076ef62f Eliminar Dockerfile 2026-06-21 10:38:27 +00:00
Aria 6bb02171fe Añadir src/pages/index.tsx 2026-06-21 10:20:43 +00:00
Aria d0441abddd Eliminar src/pages/index.tsx 2026-06-21 10:20:06 +00:00
Aria e2b4cbeff8 Añadir src/pages/api/clean-video.ts 2026-06-21 09:59:10 +00:00
Aria d0dbc27018 Eliminar src/pages/api/clean-video.ts 2026-06-21 09:58:45 +00:00
Aria 83dc5f2640 Añadir src/pages/api/clean-video.ts 2026-06-20 22:24:35 +00:00
Aria 01b23ddaff Eliminar src/pages/api/clean-video.ts 2026-06-20 22:24:15 +00:00
Aria 1bbab0c079 Añadir src/pages/index.tsx 2026-06-20 21:56:56 +00:00
Aria 164aaec4a5 Eliminar src/pages/index.tsx 2026-06-20 21:56:21 +00:00
Aria 4edcfbad5a Añadir src/pages/api/clean-video.ts 2026-06-20 21:41:17 +00:00
Aria dfa7e39a29 Eliminar src/pages/api/clean-video.ts 2026-06-20 21:40:53 +00:00
Aria 4248430a61 Añadir src/pages/index.tsx 2026-06-20 21:40:32 +00:00
Aria f27c9a546b Eliminar src/pages/index.tsx 2026-06-20 21:39:53 +00:00
Aria 7f68e17408 Actualizar src/pages/index.tsx 2026-06-20 21:30:26 +00:00
Aria e5e8a2d412 Añadir src/pages/api/clean-video.ts 2026-06-20 21:29:20 +00:00
Aria 944cbbb551 Eliminar src/pages/api/clean-video.ts 2026-06-20 21:28:39 +00:00
Aria cf07703635 Actualizar src/pages/api/clean-video.ts 2026-06-20 21:28:30 +00:00
Aria d2f50b0ded Añadir src/pages/index.tsx 2026-06-20 05:15:29 +00:00
Aria ff199d699d Eliminar src/pages/index.tsx 2026-06-20 05:15:10 +00:00
Aria 1e01e49215 Añadir src/pages/index.tsx 2026-06-20 05:07:11 +00:00
Aria ae0b5358bf Eliminar src/pages/index.tsx 2026-06-20 05:06:07 +00:00
Aria b775544cb1 Actualizar Dockerfile 2026-06-20 04:52:18 +00:00
Aria e8d78c9c2a Añadir Dockerfile 2026-06-20 04:45:47 +00:00
Aria 247d1a9654 Eliminar Dockerfile 2026-06-20 04:45:12 +00:00
Aria fc896974cc Añadir Dockerfile 2026-06-20 04:42:46 +00:00
Aria 35da60d8da Eliminar Dockerfile 2026-06-20 04:42:16 +00:00
Aria 21656df57a Añadir Dockerfile 2026-06-20 04:37:26 +00:00
Aria 3d9ea48f78 Añadir src/pages/api/clean-video.ts 2026-06-20 04:33:45 +00:00
Aria 4230c10589 Eliminar src/pages/api/clean-video.ts 2026-06-20 04:33:08 +00:00
Aria 86f3b4425c Añadir src/pages/index.tsx 2026-06-20 04:27:24 +00:00
Aria a963a221cb Eliminar src/pages/index.tsx 2026-06-20 04:27:01 +00:00
Aria 9da6b5bf51 Añadir src/pages/index.tsx 2026-06-20 04:19:18 +00:00
Aria b979e7c640 Eliminar src/pages/index.tsx 2026-06-20 04:18:49 +00:00
Aria c8846139c2 Añadir src/pages/index.tsx 2026-06-20 04:01:54 +00:00
Aria 3471f9fc9c Eliminar src/pages/index.tsx 2026-06-20 04:01:29 +00:00
Aria 942610079b Añadir src/pages/index.tsx 2026-06-20 03:43:45 +00:00
Aria 13e1f7bc93 Eliminar src/pages/index.tsx 2026-06-20 03:43:20 +00:00
Aria 8b8f66d249 Añadir src/pages/api/clean-video.ts 2026-06-20 03:07:39 +00:00
Aria 43e8bcfdc7 Eliminar src/pages/api/clean-video.ts 2026-06-20 03:06:45 +00:00
Aria ff3883a4e3 Añadir src/pages/index.tsx 2026-06-20 02:55:55 +00:00
Aria 660ac13078 Eliminar src/pages/index.tsx 2026-06-20 02:55:27 +00:00
Aria 5fac9f7caf Añadir src/pages/index.tsx 2026-06-20 02:47:34 +00:00
Aria cd3cced675 Eliminar src/pages/index.tsx 2026-06-20 02:47:04 +00:00
Aria 3713cc0cd2 Añadir src/pages/api/clean-video.ts 2026-06-19 22:43:19 +00:00
Aria a47c519b2b Eliminar src/pages/api/clean-video.ts 2026-06-19 22:42:54 +00:00
Aria 79c3d984ef Añadir src/pages/api/clean-video.ts 2026-06-19 22:31:01 +00:00
Aria 186664d9d9 Eliminar src/pages/api/clean-video.ts 2026-06-19 22:30:31 +00:00
Aria 50e37777be Añadir package.json 2026-06-19 22:29:33 +00:00
Aria f34f4cafe9 Eliminar package.json 2026-06-19 22:28:43 +00:00
Aria 4d7b0f6f58 Añadir src/pages/api/clean-video.ts 2026-06-19 22:08:17 +00:00
Aria f3fc499878 Eliminar src/pages/api/clean-video.ts 2026-06-19 22:07:50 +00:00
Aria fb7e8a4c87 Añadir package.json 2026-06-19 22:00:35 +00:00
Aria eee4c47081 Eliminar package.json 2026-06-19 22:00:13 +00:00
Aria 599e851187 Actualizar package.json 2026-06-19 21:56:01 +00:00
Aria 3b63fd3904 Añadir src/pages/api/clean-video.ts 2026-06-19 21:33:35 +00:00
Aria 5f168319f7 Eliminar src/pages/api/clean-video.ts 2026-06-19 21:33:10 +00:00
Aria b6a7c777ba Actualizar package.json 2026-06-19 21:31:21 +00:00
Aria 4a543af676 Añadir src/pages/api/clean-video.ts 2026-06-19 21:20:05 +00:00
Aria e86690522b Eliminar src/pages/api/clean-video.ts 2026-06-19 21:19:16 +00:00
Aria f320477a85 Añadir src/pages/api/clean-video.ts 2026-06-19 20:53:20 +00:00
Aria 70227a249f Añadir src/pages/index.tsx 2026-06-19 20:51:54 +00:00
Aria 90f09f9ed9 Eliminar src/pages/index.tsx 2026-06-19 20:51:22 +00:00
Aria 667ee4874e Añadir src/pages/index.tsx 2026-06-19 17:41:51 +00:00
Aria a571f7a908 Eliminar src/pages/index.tsx 2026-06-19 17:39:39 +00:00
Aria 0bc6a24d49 Añadir src/pages/index.tsx 2026-06-19 17:29:12 +00:00
Aria 47a850fcfb Eliminar src/pages/index.tsx 2026-06-19 17:28:44 +00:00
Aria 9b4cd38823 Añadir public/manifest.json 2026-06-19 17:25:54 +00:00
Aria 8384176d06 Añadir src/pages/index.tsx 2026-06-19 17:07:50 +00:00
Aria 14a4427508 Eliminar src/pages/index.tsx 2026-06-19 17:07:17 +00:00
Aria 79a9edd490 Actualizar public/assets/imagenes/Icono-intro.jpeg 2026-06-19 17:00:06 +00:00
Aria 5247e0f9b5 Actualizar public/assets/imagenes/icono-9-16.jpeg 2026-06-19 16:58:53 +00:00
Aria 45cb2ff33a Eliminar public/assets/imagenes/IMAGENES 2026-06-19 16:57:54 +00:00
Aria e21acbb13c Subir archivos a "public/assets/imagenes" 2026-06-19 16:57:35 +00:00
Aria 9e89b783d1 Añadir public/assets/imagenes/IMAGENES 2026-06-19 16:56:33 +00:00
JonnyBurger 9eecdfc4f6 Update template 2026-05-01 09:08:00 +02:00
Jonny Burger d28f2201f1 Update template 2026-03-19 13:44:10 +01:00
JonnyBurger 9b9ad8c0d8 Update template 2026-02-26 17:36:46 +01:00
Jonny Burger ebacbb4e06 Update template 2026-02-20 16:49:41 +01:00
Jonny Burger 39a51285d7 Update template 2026-02-18 14:08:17 +01:00
Jonny Burger 7a3bb60f7e Update template 2026-02-11 09:55:25 +01:00
Jonny Burger 530aae9d13 Update template 2026-02-11 09:53:54 +01:00
Jonny Burger a9d35b5128 Update template 2026-02-10 19:38:00 +01:00
Jonny Burger ee382d30dc Update template 2026-01-29 17:42:59 +01:00
JonnyBurger bbcbebe701 Update template 2025-12-17 21:35:10 +01:00
JonnyBurger 011d25b73e Update template 2025-10-25 13:46:34 +02:00
JonnyBurger 90c18e2365 Update template 2025-10-18 13:09:09 +02:00
JonnyBurger e1292822ed Update template 2025-07-24 11:51:06 +02:00
Jonny Burger 8d3f0e22ba Update template 2025-04-16 10:27:39 +02:00
Jonny Burger 074b7d7c79 Update template 2025-04-01 16:37:42 +02:00
Jonny Burger 47cfee5ea7 Update template 2025-03-30 14:33:19 +02:00
Jonny Burger 516389187e Update template 2025-02-06 08:15:13 +01:00
Jonny Burger fb0a33d870 Update template 2025-02-06 08:02:57 +01:00
Jonny Burger 0968433521 Update template 2025-01-19 11:02:23 +01:00
Jonny Burger c100a9261e Update template for React 19 2024-12-06 10:59:33 +01:00
Jonny Burger 192d48f797 Update template 2024-11-07 17:46:41 +01:00
Jonny Burger 6eafc882c9 Merge pull request #10 from remotion-dev/23730166570927191 2024-10-23 14:22:30 +05:30
Jonny Burger 0b1960d813 Update template 2024-10-23 14:21:41 +05:30
Jonny Burger 99a9c563e9 Merge pull request #9 from remotion-dev/12238114037090975 2024-10-14 15:38:57 +02:00
Jonny Burger 0f4426d12c Update template 2024-10-14 15:33:16 +02:00
Jonny Burger 3ec22bf832 Merge pull request #8 from remotion-dev/5801504067173469 2024-09-27 21:49:13 +02:00
Jonny Burger 9cbab5b624 Update template 2024-09-27 21:47:34 +02:00
Jonny Burger c85cfc620e Merge pull request #7 from remotion-dev/9685402133562709 2024-09-27 20:15:24 +02:00
Jonny Burger e1a665a947 Update template 2024-09-27 20:14:44 +02:00
Jonny Burger c09b458cea Merge pull request #6 from remotion-dev/17577337380796187 2024-09-27 17:43:03 +02:00
42 changed files with 1003 additions and 155 deletions
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "next",
"plugins": ["@remotion"],
"overrides": [
{
"files": ["remotion/**/*.{ts,tsx}"],
"extends": ["plugin:@remotion/recommended"]
}
]
}
+28
View File
@@ -0,0 +1,28 @@
# CAPA 1: Dependencias (Se congela en caché a menos que instales algo nuevo)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
# CAPA 2: Compilación (Solo actualiza tu código nuevo)
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# CAPA 3: Producción (El motor ligero final)
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# 👉 AQUÍ ESTÁ LA MAGIA: Instalamos FFMPEG globalmente en Alpine
RUN apk add --no-cache ffmpeg
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "start"]
+13 -1
View File
@@ -33,7 +33,19 @@ npm run dev
Open the Remotion Studio:
```
npm run remotion
npx remotion studio
```
Render a video locally:
```
npx remotion render
```
Upgrade Remotion:
```
npx remotion upgrade
```
The following script will set up your Remotion Bundle and Lambda function on AWS:
+1 -3
View File
@@ -1,5 +1,3 @@
import { VERSION } from "remotion/version";
/**
* Use autocomplete to get a list of available regions.
* @type {import('@remotion/lambda').AwsRegion}
@@ -8,5 +6,5 @@ export const REGION = "us-east-1";
export const SITE_NAME = "my-next-app";
export const RAM = 3009;
export const DISK = 2048;
export const DISK = 10240;
export const TIMEOUT = 240;
+10 -9
View File
@@ -5,18 +5,18 @@ import {
} from "@remotion/lambda";
import dotenv from "dotenv";
import path from "path";
import { RAM, REGION, SITE_NAME, TIMEOUT } from "./config.mjs";
import { DISK, RAM, REGION, SITE_NAME, TIMEOUT } from "./config.mjs";
console.log("Selected region:", REGION);
dotenv.config();
dotenv.config({quiet: true});
if (!process.env.AWS_ACCESS_KEY_ID && !process.env.REMOTION_AWS_ACCESS_KEY_ID) {
console.log(
'The environment variable "REMOTION_AWS_ACCESS_KEY_ID" is not set.'
'The environment variable "REMOTION_AWS_ACCESS_KEY_ID" is not set.',
);
console.log("Lambda renders were not set up.");
console.log(
"Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup"
"Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup",
);
process.exit(0);
}
@@ -25,11 +25,11 @@ if (
!process.env.REMOTION_AWS_SECRET_ACCESS_KEY
) {
console.log(
'The environment variable "REMOTION_REMOTION_AWS_SECRET_ACCESS_KEY" is not set.'
'The environment variable "REMOTION_REMOTION_AWS_SECRET_ACCESS_KEY" is not set.',
);
console.log("Lambda renders were not set up.");
console.log(
"Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup"
"Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup",
);
process.exit(0);
}
@@ -42,10 +42,11 @@ const { functionName, alreadyExisted: functionAlreadyExisted } =
memorySizeInMb: RAM,
region: REGION,
timeoutInSeconds: TIMEOUT,
diskSizeInMb: DISK,
});
console.log(
functionName,
functionAlreadyExisted ? "(already existed)" : "(created)"
functionAlreadyExisted ? "(already existed)" : "(created)",
);
process.stdout.write("Ensuring bucket... ");
@@ -55,13 +56,13 @@ const { bucketName, alreadyExisted: bucketAlreadyExisted } =
});
console.log(
bucketName,
bucketAlreadyExisted ? "(already existed)" : "(created)"
bucketAlreadyExisted ? "(already existed)" : "(created)",
);
process.stdout.write("Deploying site... ");
const { siteName } = await deploySite({
bucketName,
entryPoint: path.join(process.cwd(), "remotion", "index.ts"),
entryPoint: path.join(process.cwd(), "src", "remotion", "index.ts"),
siteName: SITE_NAME,
region: REGION,
});
+53
View File
@@ -0,0 +1,53 @@
import js from "@eslint/js";
import nextPlugin from "@next/eslint-plugin-next";
import remotion from "@remotion/eslint-plugin";
import tseslint from "typescript-eslint";
// Build Next.js recommended rules and an "off" map for overrides
const nextRecommended = nextPlugin.configs.recommended ?? { rules: {} };
const nextRecommendedRules = nextRecommended.rules ?? {};
const offNextRules = Object.fromEntries(
Object.keys(nextRecommendedRules).map((k) => [k, "off"]),
);
export default [
// Global ignores
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
"deploy.mjs",
"next.config.js",
],
},
// Base JS recommended
js.configs.recommended,
// TypeScript recommended (non type-checked for speed/simplicity)
...tseslint.configs.recommended,
// Next.js recommended rules applied to app code
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: { "@next/next": nextPlugin },
rules: {
...nextRecommendedRules,
},
},
// Remotion rules applied only to remotion files
{
files: ["src/remotion/**"],
...remotion.flatPlugin,
rules: {
...remotion.flatPlugin.rules,
},
},
// Disable all Next.js rules within remotion files
{
files: ["src/remotion/**"],
rules: {
...offNextRules,
},
},
];
+2 -3
View File
@@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
};
module.exports = nextConfig
module.exports = nextConfig;
+22 -14
View File
@@ -19,23 +19,31 @@
"@remotion/paths": "^4.0.0",
"@remotion/player": "^4.0.0",
"@remotion/shapes": "^4.0.0",
"next": "14.2.13",
"react": "18.3.1",
"react-dom": "18.3.1",
"@supabase/supabase-js": "^2.43.4",
"formidable": "^3.5.1",
"next": "16.2.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"remotion": "^4.0.0",
"zod": "3.22.3"
"zod": "4.3.6"
},
"devDependencies": {
"dotenv": "16.0.3",
"@remotion/eslint-config": "^4.0.0",
"@eslint/eslintrc": "3.1.0",
"@eslint/js": "9.38.0",
"@next/eslint-plugin-next": "15.1.6",
"@remotion/eslint-plugin": "^4.0.0",
"@types/node": "20.14.11",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@types/web": "0.0.78",
"eslint": "8.56.0",
"eslint-config-next": "14.2.13",
"typescript": "5.3.3",
"prettier": "3.2.5"
"@types/formidable": "^3.4.5",
"@types/node": "20.12.14",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/web": "0.0.166",
"@typescript-eslint/eslint-plugin": "8.46.0",
"@typescript-eslint/parser": "8.46.0",
"dotenv": "17.3.1",
"eslint": "9.19.0",
"eslint-config-next": "15.1.6",
"prettier": "3.8.1",
"typescript": "5.9.3",
"typescript-eslint": "8.46.0"
}
}
-87
View File
@@ -1,87 +0,0 @@
import { Player } from "@remotion/player";
import type { NextPage } from "next";
import Head from "next/head";
import React, { useMemo, useState } from "react";
import { Main } from "../remotion/MyComp/Main";
import {
CompositionProps,
defaultMyCompProps,
DURATION_IN_FRAMES,
VIDEO_FPS,
VIDEO_HEIGHT,
VIDEO_WIDTH,
} from "../types/constants";
import { z } from "zod";
import { RenderControls } from "../components/RenderControls";
import { Tips } from "../components/Tips/Tips";
import { Spacing } from "../components/Spacing";
const container: React.CSSProperties = {
maxWidth: 768,
margin: "auto",
marginBottom: 20,
};
const outer: React.CSSProperties = {
borderRadius: "var(--geist-border-radius)",
overflow: "hidden",
boxShadow: "0 0 200px rgba(0, 0, 0, 0.15)",
marginBottom: 40,
marginTop: 60,
};
const player: React.CSSProperties = {
width: "100%",
};
const Home: NextPage = () => {
const [text, setText] = useState<string>(defaultMyCompProps.title);
const inputProps: z.infer<typeof CompositionProps> = useMemo(() => {
return {
title: text,
};
}, [text]);
return (
<div>
<Head>
<title>Remotion and Next.js</title>
<meta name="description" content="Remotion and Next.js" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<div style={container}>
<div className="cinematics" style={outer}>
<Player
component={Main}
inputProps={inputProps}
durationInFrames={DURATION_IN_FRAMES}
fps={VIDEO_FPS}
compositionHeight={VIDEO_HEIGHT}
compositionWidth={VIDEO_WIDTH}
style={player}
controls
autoPlay
loop
/>
</div>
<RenderControls
text={text}
setText={setText}
inputProps={inputProps}
></RenderControls>
<Spacing></Spacing>
<Spacing></Spacing>
<Spacing></Spacing>
<Spacing></Spacing>
<Tips></Tips>
</div>
</div>
);
};
export default Home;
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+18
View File
@@ -0,0 +1,18 @@
// [BOTÓN DE COPIAR]
{
"name": "Nayla Core Editor",
"short_name": "NAYLA",
"description": "Módulo Operativo Visual",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"icons": [
{
"src": "/assets/imagenes/Icono-intro.jpeg",
"sizes": "512x512",
"type": "image/jpeg",
"purpose": "any maskable"
}
]
}
@@ -26,6 +26,8 @@ export const Input: React.FC<{
return (
<input
type="search"
autoComplete="off"
disabled={disabled}
name="title"
style={textarea}
@@ -1,6 +1,6 @@
import { z } from "zod";
import { COMP_NAME, CompositionProps } from "../../types/constants";
import { useRendering } from "../helpers/use-rendering";
import { CompositionProps, COMP_NAME } from "../types/constants";
import { AlignEnd } from "./AlignEnd";
import { Button } from "./Button/Button";
import { InputContainer } from "./Container";
@@ -1,6 +1,6 @@
import React, { useMemo } from "react";
import { makeRect } from "@remotion/shapes";
import { translatePath } from "@remotion/paths";
import { makeRect } from "@remotion/shapes";
import React, { useMemo } from "react";
import styles from "./styles.module.css";
const viewBox = 100;
@@ -1,7 +1,7 @@
import { z } from "zod";
import { useCallback, useMemo, useState } from "react";
import { z } from "zod";
import { CompositionProps } from "../../types/constants";
import { getProgress, renderVideo } from "../lambda/api";
import { CompositionProps } from "../types/constants";
export type State =
| {
+3 -3
View File
@@ -1,11 +1,11 @@
import { z } from "zod";
import type { RenderMediaOnLambdaOutput } from "@remotion/lambda/client";
import { z } from "zod";
import { CompositionProps } from "../../types/constants";
import {
ProgressRequest,
ProgressResponse,
RenderRequest,
} from "../types/schema";
import { CompositionProps } from "../types/constants";
} from "../../types/schema";
import { ApiResponse } from "../helpers/api-response";
const makeRequest = async <Res>(
+1 -1
View File
@@ -1,5 +1,5 @@
import "../styles/global.css";
import type { AppProps } from "next/app";
import "../../styles/global.css";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
+206
View File
@@ -0,0 +1,206 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { createClient } from '@supabase/supabase-js';
import { exec } from 'child_process';
import util from 'util';
const execPromise = util.promisify(exec);
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Comando denegado. Se requiere método POST.' });
}
try {
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const replicateToken = process.env.REPLICATE_API_TOKEN;
const falKey = process.env.FAL_KEY;
if (!supabaseUrl || !supabaseKey) {
return res.status(500).json({ error: 'Llaves de almacenamiento (Supabase) ausentes.' });
}
const supabase = createClient(supabaseUrl, supabaseKey);
const form = formidable({
keepExtensions: true,
maxFileSize: 50 * 1024 * 1024,
});
const [fields, files] = await form.parse(req);
const videoUpload = Array.isArray(files.video) ? files.video[0] : files.video;
const coordenadasRaw = Array.isArray(fields.coordenadas) ? fields.coordenadas[0] : fields.coordenadas;
const coordenadas = coordenadasRaw ? JSON.parse(coordenadasRaw) : [];
// Leemos si el usuario presionó el botón de "Local" o "Nube"
const motorDeseadoRaw = Array.isArray(fields.motor) ? fields.motor[0] : fields.motor;
const motorDeseado = motorDeseadoRaw || 'nube'; // Por defecto se va a la nube
if (!videoUpload) {
return res.status(400).json({ error: 'Video no detectado.' });
}
// ============================================================================
// 🛠️ MOTOR 1: PROCESAMIENTO LOCAL (FFMPEG)
// ============================================================================
if (motorDeseado === 'local') {
console.log("Iniciando procesamiento LOCAL con FFMPEG...");
const outputPath = path.join(os.tmpdir(), `cleaned-${Date.now()}.mp4`);
// Armamos los filtros de difuminado (delogo) usando las coordenadas del frontend
let filterString = '';
if (coordenadas.length > 0) {
filterString = coordenadas.map((c: any) =>
// CORRECCIÓN: Código moderno, limpio y sin el parámetro obsoleto 'band'
`delogo=x=${Math.floor(c.x)}:y=${Math.floor(c.y)}:w=${Math.floor(c.width)}:h=${Math.floor(c.height)}`
).join(',');
} else {
// Coordenadas de emergencia
filterString = 'delogo=x=750:y=200:w=250:h=90,delogo=x=400:y=1700:w=280:h=90';
}
// Alta calidad de renderizado activada
const comando = `ffmpeg -y -i "${videoUpload.filepath}" -vf "${filterString}" -c:v libx264 -preset slow -crf 18 -c:a copy "${outputPath}"`;
try {
await execPromise(comando);
const fileContentLocal = fs.readFileSync(outputPath);
const localUploadName = `video-local-${Date.now()}.mp4`;
const { error: localUploadError } = await supabase.storage
.from('temp-videos')
.upload(localUploadName, fileContentLocal, {
contentType: 'video/mp4',
upsert: true
});
if (localUploadError) throw new Error("Supabase rechazó el archivo local.");
const { data: localUrlData } = supabase.storage
.from('temp-videos')
.getPublicUrl(localUploadName);
fs.unlinkSync(videoUpload.filepath);
fs.unlinkSync(outputPath);
return res.status(200).json({
success: true,
motor: 'local_ffmpeg',
url: localUrlData.publicUrl,
status: 'succeeded'
});
} catch (localError) {
console.error("FFMPEG falló localmente:", localError);
if (fs.existsSync(videoUpload.filepath)) fs.unlinkSync(videoUpload.filepath);
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
throw new Error("Fallo en el procesamiento local. ¿Está instalado ffmpeg en el Docker?");
}
}
// ============================================================================
// ☁️ MOTOR 2: PROCESAMIENTO EN LA NUBE (REPLICATE / FAL)
// ============================================================================
const fileContent = fs.readFileSync(videoUpload.filepath);
const nombreUnico = `video-${Date.now()}.mp4`;
const { error: uploadError } = await supabase.storage
.from('temp-videos')
.upload(nombreUnico, fileContent, {
contentType: 'video/mp4',
upsert: true
});
if (uploadError) throw new Error("Supabase rechazó el archivo.");
const { data: urlData } = supabase.storage
.from('temp-videos')
.getPublicUrl(nombreUnico);
fs.unlinkSync(videoUpload.filepath);
const videoUrl = urlData.publicUrl;
const maskBoxes = coordenadas.map((c: any) => [c.x, c.y, c.width, c.height]);
// --- REPLICATE ---
if (replicateToken) {
console.log("Intentando procesar con Replicate...");
const replicateResponse = await fetch("https://api.replicate.com/v1/predictions", {
method: "POST",
headers: {
"Authorization": `Token ${replicateToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
version: "ccb3b1e360fbfbc7e5cfa8e718873fb2a20fc1d0c5a2c27732a31dcce411bd13",
input: {
video: videoUrl,
masks: JSON.stringify(maskBoxes)
}
})
});
const replicateData = await replicateResponse.json();
if (replicateResponse.ok) {
return res.status(200).json({
success: true,
motor: 'replicate',
url: videoUrl,
prediction_id: replicateData.id,
status: replicateData.status
});
} else {
console.warn("Replicate falló. Motivo:", replicateData.detail || "Desconocido");
}
}
// --- FAL.AI (Respaldo) ---
if (falKey) {
console.log("Activando rotación a FAL...");
const falResponse = await fetch("https://queue.fal.run/fal-ai/fast-video-inpaint", {
method: "POST",
headers: {
"Authorization": `Key ${falKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_url: videoUrl,
mask_coordinates: maskBoxes
})
});
const falData = await falResponse.json();
if (falResponse.ok) {
return res.status(200).json({
success: true,
motor: 'fal',
url: videoUrl,
prediction_id: falData.request_id,
status: 'processing'
});
} else {
throw new Error("Ambos motores de IA (Replicate y FAL) rechazaron la orden.");
}
}
throw new Error("No hay llaves configuradas para ninguna inteligencia artificial.");
} catch (error) {
console.error("Error crítico en la tubería:", error);
return res.status(500).json({ error: 'Error interno del servidor. Revisa los logs.' });
}
}
@@ -1,11 +1,11 @@
import {
speculateFunctionName,
AwsRegion,
getRenderProgress,
speculateFunctionName,
} from "@remotion/lambda/client";
import { DISK, RAM, REGION, TIMEOUT } from "../../../config.mjs";
import { DISK, RAM, REGION, TIMEOUT } from "../../../../config.mjs";
import { ProgressRequest, ProgressResponse } from "../../../../types/schema";
import { executeApi } from "../../../helpers/api-response";
import { ProgressRequest, ProgressResponse } from "../../../types/schema";
const progress = executeApi<ProgressResponse, typeof ProgressRequest>(
ProgressRequest,
@@ -1,11 +1,12 @@
import { AwsRegion, RenderMediaOnLambdaOutput } from "@remotion/lambda/client";
import {
AwsRegion,
renderMediaOnLambda,
RenderMediaOnLambdaOutput,
speculateFunctionName,
} from "@remotion/lambda/client";
import { DISK, RAM, REGION, SITE_NAME, TIMEOUT } from "../../../config.mjs";
import { DISK, RAM, REGION, SITE_NAME, TIMEOUT } from "../../../../config.mjs";
import { RenderRequest } from "../../../../types/schema";
import { executeApi } from "../../../helpers/api-response";
import { RenderRequest } from "../../../types/schema";
const render = executeApi<RenderMediaOnLambdaOutput, typeof RenderRequest>(
RenderRequest,
+51
View File
@@ -0,0 +1,51 @@
// @ts-nocheck
/* eslint-disable */
import type { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { createClient } from '@supabase/supabase-js';
import { exec } from 'child_process';
import util from 'util';
const execPromise = util.promisify(exec);
export default async function handler(req, res) {
if (req.method !== 'POST') return res.status(405).json({ error: 'Usa POST.' });
try {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) return res.status(500).json({ error: 'Llaves ausentes.' });
const supabase = createClient(supabaseUrl, supabaseKey);
const { pistaVideo } = req.body;
if (!pistaVideo || pistaVideo.length === 0) return res.status(400).json({ error: 'Pista vacía.' });
const jobId = Date.now().toString();
const tmpDir = os.tmpdir();
const archivosLocales = [];
for (let i = 0; i < pistaVideo.length; i++) {
const clip = pistaVideo[i];
if (clip.tipo !== 'video') continue;
const resArchivo = await fetch(clip.url);
const buffer = await resArchivo.arrayBuffer();
const rutaLocal = path.join(tmpDir, `clip_${jobId}_${i}.mp4`);
fs.writeFileSync(rutaLocal, new Uint8Array(buffer));
archivosLocales.push(rutaLocal);
}
if (archivosLocales.length === 0) return res.status(400).json({ error: 'Sin videos válidos.' });
const listaTxtPath = path.join(tmpDir, `lista_${jobId}.txt`);
fs.writeFileSync(listaTxtPath, archivosLocales.map(r => `file '${r}'`).join('\n'));
const outputPath = path.join(tmpDir, `final_${jobId}.mp4`);
await execPromise(`ffmpeg -y -f concat -safe 0 -i "${listaTxtPath}" -c copy "${outputPath}"`);
const fileContentFinal = fs.readFileSync(outputPath);
const nombreFinal = `render-maestro-${jobId}.mp4`;
const { error: uploadError } = await supabase.storage.from('temp-videos').upload(nombreFinal, fileContentFinal, { contentType: 'video/mp4', upsert: true });
if (uploadError) throw uploadError;
const { data: urlData } = supabase.storage.from('temp-videos').getPublicUrl(nombreFinal);
archivosLocales.forEach(r => fs.unlinkSync(r));
fs.unlinkSync(listaTxtPath);
fs.unlinkSync(outputPath);
return res.status(200).json({ success: true, url: urlData.publicUrl });
} catch (error) {
return res.status(500).json({ error: error.message || 'Error en renderizado.' });
}
}
+554
View File
@@ -0,0 +1,554 @@
// @ts-nocheck
/* eslint-disable */
import React, { useState, useEffect, useRef } from 'react';
import Head from 'next/head';
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://dummy.supabase.co';
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'dummy_key';
const supabase = process.env.NEXT_PUBLIC_SUPABASE_URL
? createClient(supabaseUrl, supabaseAnonKey)
: {
auth: {
getSession: async () => ({ data: { session: { user: { email: 'dev@test.com' } } } }),
signInWithOtp: async () => ({ error: null }),
verifyOtp: async () => ({ error: null })
}
} as unknown as ReturnType<typeof createClient>;
type Rect = { id: string; x: number; y: number; width: number; height: number };
type MediaItem = { id: string; url: string; tipo: 'foto' | 'video' | 'audio'; nombre: string; creado_en: string; esOverlay: boolean; etiqueta: string };
type TimelineItem = { id: string; mediaId: string; tipo: 'foto' | 'video' | 'audio'; nombre: string; etiqueta: string; url: string };
type MarcoConfig = {
posicion: 'derecha' | 'izquierda' | 'abajo' | 'arriba' | 'derecha+abajo' | 'derecha+arriba' | 'izquierda+abajo' | 'izquierda+arriba';
grosor: number;
color: string;
};
const TOOLS = [
{ id: 'galeria', nombre: 'BODEGA', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> },
{ id: 'cortar', nombre: 'CORTAR', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> },
{ id: 'dividir', nombre: 'DIVIDIR', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="12" y1="2" x2="12" y2="22"/><line x1="4" y1="12" x2="8" y2="12"/><line x1="16" y1="12" x2="20" y2="12"/></svg> },
{ id: 'borrar', nombre: 'BORRAR', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> },
{ id: 'volumen', nombre: 'VOLUMEN', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg> },
{ id: 'velocidad', nombre: 'VELOCIDAD', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> },
{ id: 'girar', nombre: 'GIRAR', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><polyline points="3 3 3 8 8 8"/></svg> },
{ id: 'duplicar', nombre: 'DUPLICAR', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> },
{ id: 'inverso', nombre: 'INVERSO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><polyline points="21 3 21 8 16 8"/></svg> },
{ id: 'congelacion', nombre: 'CONGELACIÓN', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2v20M2 12h20M4.93 4.93l14.14 14.14M19.07 4.93L4.93 19.07"/></svg> },
{ id: 'mascara', nombre: 'MÁSCARA', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> },
{ id: 'opacidad', nombre: 'OPACIDAD', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="2" x2="12" y2="22"/><line x1="12" y1="12" x2="22" y2="12"/></svg> },
{ id: 'filtro', nombre: 'FILTRO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg> },
{ id: 'efecto', nombre: 'EFECTO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10 3.2A1.2 1.2 0 0 0 8 4v16a1.2 1.2 0 0 0 2 1.2M14 3.2A1.2 1.2 0 0 1 16 4v16a1.2 1.2 0 0 1-2 1.2M6 8H4M6 16H4M20 8h-2M20 16h-2"/></svg> },
{ id: 'texto', nombre: 'TEXTO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg> },
{ id: 'audio', nombre: 'AUDIO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg> },
{ id: 'lona', nombre: 'LONA/RATIO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg> },
{ id: 'marco', nombre: 'MARCO', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="2" y="2" width="20" height="20" rx="2"/><rect x="6" y="6" width="12" height="12" rx="1"/></svg> },
{ id: 'herramientas', nombre: 'DELOGO IA ⚡', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> },
{ id: 'script', nombre: 'SCRIPT 💻', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> },
{ id: 'ia', nombre: 'SUPERVISOR IA', icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/><line x1="8" y1="16" x2="8" y2="16"/><line x1="16" y1="16" x2="16" y2="16"/></svg> },
];
export default function NaylaCore() {
const [session, setSession] = useState(null);
const [emailInput, setEmailInput] = useState('');
const [otpInput, setOtpInput] = useState('');
const [otpEnviado, setOtpEnviado] = useState(false);
const [authLoading, setAuthLoading] = useState(false);
const [message, setMessage] = useState('');
const [navActiva, setNavActiva] = useState<string | null>(null);
const [codigoJsInput, setCodigoJsInput] = useState('// Inyecta comandos JS aquí\nNaylaEngine.unir("V1", "V2");');
const [toolMessage, setToolMessage] = useState<string | null>(null);
const [marcoConfig, setMarcoConfig] = useState<MarcoConfig>({ posicion: 'derecha+abajo', grosor: 80, color: '#ffffff' });
const [marcoImagenes, setMarcoImagenes] = useState<{ original: string; procesada: string; nombre: string }[]>([]);
const [marcoProcesando, setMarcoProcesando] = useState(false);
const [galeriaMultimedia, setGaleriaMultimedia] = useState<MediaItem[]>([]);
const [lineaDeTiempo, setLineaDeTiempo] = useState<TimelineItem[]>([]);
const [clipSeleccionado, setClipSeleccionado] = useState<string | null>(null);
const [canvasRatio, setCanvasRatio] = useState<'9/16' | '16/9' | '1/1' | '4/5'>('9/16');
const [calidadExportacion, setCalidadExportacion] = useState('1080p');
const [showIntro, setShowIntro] = useState(true);
const [videoFile, setVideoFile] = useState<File | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [videoTerminado, setVideoTerminado] = useState<string | null>(null);
const [videoResultadoUrl, setVideoResultadoUrl] = useState<string | null>(null);
const [videoMetadata, setVideoMetadata] = useState({ width: 1080, height: 1920 });
const [isPlaying, setIsPlaying] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const timelineRef = useRef<HTMLDivElement>(null);
const [rects, setRects] = useState<Rect[]>([]);
const [isDrawing, setIsDrawing] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [currentRect, setCurrentRect] = useState<Rect | null>(null);
const [resizingInfo, setResizingInfo] = useState<{ id: string, corner: string } | null>(null);
const [draggingInfo, setDraggingInfo] = useState<{ id: string, offsetX: number, offsetY: number } | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => setSession(session));
const timer = setTimeout(() => setShowIntro(false), 3000);
return () => clearTimeout(timer);
}, []);
const aplicarMarcoAImagen = (imagenUrl: string, config: MarcoConfig): Promise<string> => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return resolve(imagenUrl);
let targetW = 1080, targetH = 1920;
if (canvasRatio === '16/9') { targetW = 1920; targetH = 1080; }
if (canvasRatio === '1/1') { targetW = 1080; targetH = 1080; }
if (canvasRatio === '4/5') { targetW = 1080; targetH = 1350; }
canvas.width = targetW; canvas.height = targetH;
ctx.fillStyle = config.color;
ctx.fillRect(0, 0, targetW, targetH);
const g = config.grosor;
let imgX = 0, imgY = 0, imgW = targetW, imgH = targetH;
const pos = config.posicion;
if (pos.includes('derecha')) imgW -= g;
if (pos.includes('izquierda')) { imgX += g; imgW -= g; }
if (pos.includes('abajo')) imgH -= g;
if (pos.includes('arriba')) { imgY += g; imgH -= g; }
const scale = Math.min(imgW / img.width, imgH / img.height);
const drawW = img.width * scale;
const drawH = img.height * scale;
ctx.drawImage(img, imgX + (imgW - drawW) / 2, imgY + (imgH - drawH) / 2, drawW, drawH);
resolve(canvas.toDataURL('image/jpeg', 0.95));
};
img.src = imagenUrl;
});
};
const procesarImagenesConMarco = async () => {
const fotos = galeriaMultimedia.filter(item => item.tipo === 'foto');
if (fotos.length === 0) return alert('No hay fotos en la bodega. Sube fotos primero.');
setMarcoProcesando(true);
const resultados = [];
for (const foto of fotos) {
const procesada = await aplicarMarcoAImagen(foto.url, marcoConfig);
resultados.push({ original: foto.url, procesada, nombre: foto.nombre });
}
setMarcoImagenes(resultados);
setMarcoProcesando(false);
};
const descargarImagenConMarco = (url: string, nombre: string) => {
const a = document.createElement('a');
a.href = url; a.download = `MARCO_${nombre.replace(/\.[^/.]+$/, '')}.jpg`; a.click();
};
const descargarTodasConMarco = () => {
marcoImagenes.forEach((img, i) => setTimeout(() => descargarImagenConMarco(img.procesada, img.nombre), i * 300));
};
const handleEmailAuth = async (e: React.FormEvent) => {
e.preventDefault();
if (!emailInput) return;
setAuthLoading(true); setMessage('');
try {
const { error } = await supabase.auth.signInWithOtp({ email: emailInput, options: { shouldCreateUser: true } });
if (error) throw error;
setOtpEnviado(true);
} catch (err) { setMessage('Error crítico de transmisión.'); }
finally { setAuthLoading(false); }
};
const handleOtpVerify = async (e: React.FormEvent) => {
e.preventDefault();
if (!otpInput || !emailInput) return;
setAuthLoading(true); setMessage('');
try {
const { error } = await supabase.auth.verifyOtp({ email: emailInput, token: otpInput, type: 'email' });
if (error) throw error;
} catch (err) { setMessage('Código incorrecto.'); }
finally { setAuthLoading(false); }
};
const handlePasteCode = async () => {
try { const text = await navigator.clipboard.readText(); if (text) setOtpInput(text.trim()); } catch (err) {}
};
const handleSubirMultimedia = async (e: React.ChangeEvent<HTMLInputElement>, tipo: 'foto' | 'video' | 'audio') => {
if (!e.target.files || e.target.files.length === 0) return;
const files = Array.from(e.target.files);
const nuevosItems: MediaItem[] = files.map((file, index) => {
const countTipo = galeriaMultimedia.filter(item => item.tipo === tipo).length + index + 1;
const inicial = tipo === 'video' ? 'V' : tipo === 'foto' ? 'F' : 'A';
return { id: (Date.now() + index).toString(), url: URL.createObjectURL(file), tipo, nombre: file.name, creado_en: new Date().toLocaleTimeString(), esOverlay: false, etiqueta: `${inicial}${countTipo}` };
});
setGaleriaMultimedia([...galeriaMultimedia, ...nuevosItems]);
if (tipo === 'video' && !videoTerminado) { setVideoFile(files[0]); setVideoTerminado(nuevosItems[0].url); setVideoResultadoUrl(null); }
};
const eliminarDeGaleria = (id: string) => {
setGaleriaMultimedia(galeriaMultimedia.filter(item => item.id !== id));
setLineaDeTiempo(lineaDeTiempo.filter(item => item.mediaId !== id));
};
const agregarAlTimeline = (item: MediaItem) => {
const nuevo: TimelineItem = { id: Date.now().toString(), mediaId: item.id, tipo: item.tipo, nombre: item.nombre, etiqueta: item.etiqueta, url: item.url };
setLineaDeTiempo([...lineaDeTiempo, nuevo]);
if (item.tipo === 'video' || item.tipo === 'foto') { setVideoTerminado(item.url); setVideoResultadoUrl(null); setRects([]); }
};
const quitarDelTimeline = (id: string) => { setLineaDeTiempo(lineaDeTiempo.filter(t => t.id !== id)); setClipSeleccionado(null); };
const renombrarItem = (id: string, nuevoNombre: string) => {
setGaleriaMultimedia(galeriaMultimedia.map(item => item.id === id ? { ...item, nombre: nuevoNombre } : item));
setLineaDeTiempo(lineaDeTiempo.map(item => item.mediaId === id ? { ...item, nombre: nuevoNombre } : item));
};
const handleToolClick = (tool: any) => {
if (navActiva === tool.id) { setNavActiva(null); setToolMessage(null); return; }
setNavActiva(tool.id);
if (['galeria', 'herramientas', 'script', 'ia', 'marco'].includes(tool.id)) setToolMessage(null);
else setToolMessage('PRÓXIMAMENTE');
};
const togglePlay = () => {
if (videoRef.current) {
if (videoRef.current.paused) { videoRef.current.play(); setIsPlaying(true); }
else { videoRef.current.pause(); setIsPlaying(false); }
}
};
const handleDescargar = () => {
const url = videoResultadoUrl || videoTerminado;
if (!url) return alert('No hay ningún video cargado para descargar.');
const a = document.createElement('a'); a.href = url; a.download = `Nayla_Export_${calidadExportacion}_${Date.now()}.mp4`; a.click();
};
const processVideo = async (motorElegido: 'nube' | 'local') => {
if (!videoFile) return alert('Por favor, sube un video primero.');
if (rects.length === 0) return alert('Dibuja al menos un recuadro sobre la marca de agua.');
setIsProcessing(true);
try {
const formData = new FormData();
formData.append('video', videoFile); formData.append('coordenadas', JSON.stringify(rects)); formData.append('motor', motorElegido);
const res = await fetch('/api/clean-video', { method: 'POST', body: formData });
const data = await res.json();
if (res.ok && data.success) { setVideoResultadoUrl(data.url); setRects([]); alert(`Supresión completada: ${motorElegido.toUpperCase()}`); }
else throw new Error(data.error || 'Fallo en el servidor');
} catch (err: any) { alert('Error: ' + err.message); }
finally { setIsProcessing(false); }
};
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (!videoTerminado || !containerRef.current || resizingInfo || draggingInfo) return;
const c = containerRef.current.getBoundingClientRect();
setStartPos({ x: e.clientX - c.left, y: e.clientY - c.top });
setIsDrawing(true);
setCurrentRect({ id: Date.now().toString(), x: e.clientX - c.left, y: e.clientY - c.top, width: 0, height: 0 });
};
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (!videoTerminado || !containerRef.current) return;
const c = containerRef.current.getBoundingClientRect();
const cx = Math.max(0, Math.min(e.clientX - c.left, c.width));
const cy = Math.max(0, Math.min(e.clientY - c.top, c.height));
if (isDrawing && currentRect) {
setCurrentRect({ ...currentRect, x: Math.min(startPos.x, cx), y: Math.min(startPos.y, cy), width: Math.abs(cx - startPos.x), height: Math.abs(cy - startPos.y) });
} else if (draggingInfo) {
setRects(rects.map(r => r.id === draggingInfo.id ? { ...r, x: Math.max(0, Math.min(cx - draggingInfo.offsetX, c.width - r.width)), y: Math.max(0, Math.min(cy - draggingInfo.offsetY, c.height - r.height)) } : r));
} else if (resizingInfo) {
setRects(rects.map(r => {
if (r.id !== resizingInfo.id) return r;
let nx = r.x, ny = r.y, nw = r.width, nh = r.height;
if (resizingInfo.corner.includes('e')) nw = Math.max(20, cx - r.x);
if (resizingInfo.corner.includes('s')) nh = Math.max(20, cy - r.y);
if (resizingInfo.corner.includes('w')) { const d = cx - r.x; nw = Math.max(20, r.width - d); if (nw > 20) nx = cx; }
if (resizingInfo.corner.includes('n')) { const d = cy - r.y; nh = Math.max(20, r.height - d); if (nh > 20) ny = cy; }
return { ...r, x: nx, y: ny, width: nw, height: nh };
}));
}
};
const handlePointerUp = () => {
if (isDrawing && currentRect && currentRect.width > 10 && currentRect.height > 10) setRects([...rects, currentRect]);
setIsDrawing(false); setCurrentRect(null); setResizingInfo(null); setDraggingInfo(null);
};
const removeRect = (id: string) => setRects(rects.filter(r => r.id !== id));
const pistaVideo = lineaDeTiempo.filter(t => t.tipo === 'video' || t.tipo === 'foto');
const pistaAudio = lineaDeTiempo.filter(t => t.tipo === 'audio');
const hayClips = pistaVideo.length > 0;
const POSICIONES: MarcoConfig['posicion'][] = ['derecha', 'izquierda', 'abajo', 'arriba', 'derecha+abajo', 'derecha+arriba', 'izquierda+abajo', 'izquierda+arriba'];
const ICONOS_POS: Record<string, string> = { derecha: '→', izquierda: '←', abajo: '↓', arriba: '↑', 'derecha+abajo': '↘', 'derecha+arriba': '↗', 'izquierda+abajo': '↙', 'izquierda+arriba': '↖' };
const globalStyles = `
::-webkit-scrollbar { height: 4px; width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #404040; border-radius: 10px; }
* { -webkit-tap-highlight-color: transparent; }
button:focus, button:active { outline: none; background-color: inherit; }
.neon-btn { background: #0a0a0a; border: 1px solid #262626; color: #a3a3a3; transition: all 0.2s ease; display: flex; justify-content: center; align-items: center; gap: 8px; }
.neon-btn:active, .neon-btn.active { background: #ffffff; color: #000000; border-color: #ffffff; box-shadow: 0 0 15px rgba(255,255,255,0.7); }
.nav-btn { font-size: 0.7rem; font-weight: bold; padding: 0.8rem 1.2rem; border-radius: 100px; cursor: pointer; text-transform: uppercase; white-space: nowrap; }
.tool-btn { background: transparent; border: none; color: #a3a3a3; display: flex; flex-direction: column; align-items: center; gap: 6px; font-size: 0.7rem; cursor: pointer; transition: 0.2s; min-width: 70px; }
.tool-btn:hover { color: #ffffff; }
.tool-icon { width: 54px; height: 54px; display: flex; justify-content: center; align-items: center; border-radius: 14px; transition: all 0.2s ease; border: 1px solid transparent; }
.tool-btn.active .tool-icon, .tool-btn:active .tool-icon { background: #ffffff !important; color: #000000 !important; box-shadow: 0 0 15px rgba(255,255,255,0.7) !important; border-color: #ffffff !important; }
.tool-btn.active span, .tool-btn:active span { color: #ffffff; font-weight: bold; }
.timeline-track { display: flex; height: 70px; overflow-x: auto; align-items: center; gap: 0; -webkit-overflow-scrolling: touch; }
.timeline-track::-webkit-scrollbar { height: 0; }
.clip-block { height: 70px; min-width: 80px; width: 80px; position: relative; cursor: pointer; flex-shrink: 0; border-top: 2px solid transparent; border-bottom: 2px solid transparent; border-right: 1px solid #000; transition: 0.2s; }
.clip-block:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
.clip-block:last-child { border-top-right-radius: 10px; border-bottom-right-radius: 10px; border-right: none; }
.clip-block.selected { border: 2px solid #ffffff; z-index: 10; box-shadow: 0 0 15px rgba(255,255,255,0.4); border-radius: 10px; }
.audio-block { height: 35px; border-radius: 8px; flex-shrink: 0; min-width: 120px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 0.65rem; cursor: pointer; margin-right: 2px; border: 1px solid #404040; }
.toolbar-container { display: flex; gap: 8px; overflow-x: auto; padding: 12px 16px; background-color: #050505; border-top: 1px solid #1a1a1a; min-height: 95px; position: relative; z-index: 100; align-items: center; }
.toolbar-container::-webkit-scrollbar { height: 0; }
.panel-container { background-color: #050505; border-top: 1px solid #1a1a1a; padding: 15px; position: absolute; bottom: 95px; left: 0; right: 0; z-index: 90; box-shadow: 0 -5px 20px rgba(0,0,0,0.8); }
.marco-pos-btn { background: #0a0a0a; border: 1px solid #262626; color: #a3a3a3; border-radius: 10px; padding: 8px 6px; font-size: 0.7rem; cursor: pointer; transition: 0.2s; text-align: center; font-weight: bold; }
.marco-pos-btn.selected { background: #ffffff; color: #000000; border-color: #ffffff; box-shadow: 0 0 10px rgba(255,255,255,0.5); }
`;
if (!session) {
if (showIntro) {
return (
<div style={{ minHeight: '100vh', backgroundColor: '#000', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<style>{`@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }`}</style>
<img src="/assets/imagenes/Icono-intro.jpeg" alt="NAYLA" style={{ width: '150px', height: '150px', borderRadius: '24px', objectFit: 'cover', animation: 'fadeIn 1s ease-in-out' }} />
</div>
);
}
return (
<div style={{ minHeight: '100vh', backgroundColor: '#000', color: '#fff', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', fontFamily: 'system-ui, sans-serif', overflow: 'hidden' }}>
<Head><title>NAYLA - AUTENTICACIÓN</title></Head>
<style>{globalStyles}</style>
<div style={{ width: '100%', maxWidth: '400px', border: '1px solid #262626', backgroundColor: '#050505', padding: '2.5rem', borderRadius: '24px', textAlign: 'center', opacity: otpEnviado ? 0.5 : 1, transition: 'opacity 0.3s' }}>
<h1 style={{ fontSize: '1rem', letterSpacing: '4px', margin: '0 0 2rem 0', textTransform: 'uppercase' }}>NAYLA</h1>
<form onSubmit={handleEmailAuth}>
<input type="email" placeholder="CORREO MAESTRO" value={emailInput} onChange={(e) => setEmailInput(e.target.value)} disabled={otpEnviado} style={{ width: '100%', padding: '1rem', backgroundColor: '#0a0a0a', border: '1px solid #404040', borderRadius: '16px', color: '#fff', fontSize: '0.8rem', marginBottom: '1rem', textAlign: 'center', outline: 'none' }} />
<button type="submit" disabled={authLoading || otpEnviado} className="neon-btn nav-btn" style={{ width: '100%', marginBottom: '1rem' }}>{authLoading && !otpEnviado ? 'PROCESANDO...' : 'SOLICITAR ACCESO'}</button>
{message && !otpEnviado && <p style={{ color: '#ff4444', fontSize: '0.8rem', margin: 0 }}>{message}</p>}
</form>
</div>
<div style={{ position: 'fixed', bottom: otpEnviado ? 0 : '-100%', left: 0, right: 0, backgroundColor: '#000', borderTop: '1px solid #fff', borderLeft: '1px solid #fff', borderRight: '1px solid #fff', borderTopLeftRadius: '24px', borderTopRightRadius: '24px', padding: '2.5rem', transition: 'bottom 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)', display: 'flex', flexDirection: 'column', alignItems: 'center', zIndex: 100 }}>
<div style={{ width: '40px', height: '4px', backgroundColor: '#fff', borderRadius: '2px', marginBottom: '2rem', opacity: 0.5 }} />
{otpEnviado && <p style={{ color: '#00ffcc', fontSize: '0.8rem', marginBottom: '1rem', letterSpacing: '1px', fontWeight: 'bold' }}>CÓDIGO ENVIADO REVISA TU CORREO</p>}
<h2 style={{ fontSize: '0.9rem', letterSpacing: '2px', margin: '0 0 1.5rem 0', textTransform: 'uppercase' }}>CÓDIGO DE ACCESO</h2>
<form onSubmit={handleOtpVerify} style={{ width: '100%', maxWidth: '350px' }}>
<input type="text" inputMode="numeric" placeholder="CÓDIGO 000000" value={otpInput} onChange={(e) => setOtpInput(e.target.value)} style={{ width: '100%', padding: '1rem', backgroundColor: '#0a0a0a', border: '1px solid #fff', borderRadius: '16px', color: '#fff', fontSize: '1rem', marginBottom: '1rem', textAlign: 'center', outline: 'none', letterSpacing: '4px' }} />
<div style={{ display: 'flex', gap: '10px', marginBottom: '1rem' }}>
<button type="button" onClick={handlePasteCode} className="neon-btn nav-btn" style={{ flex: 1, padding: '0.8rem' }}>PEGAR CÓDIGO</button>
<button type="submit" disabled={authLoading} className="neon-btn nav-btn" style={{ flex: 1, backgroundColor: '#fff', color: '#000', fontWeight: 'bold', padding: '0.8rem' }}>{authLoading ? 'VERIFICANDO...' : 'INGRESAR'}</button>
</div>
{message && otpEnviado && <p style={{ color: '#ff4444', fontSize: '0.8rem', margin: 0, textAlign: 'center' }}>{message}</p>}
</form>
</div>
</div>
);
}
return (
<div style={{ minHeight: '100vh', backgroundColor: '#000', color: '#ededed', fontFamily: 'system-ui, sans-serif' }}>
<Head><title>NAYLA CORE</title></Head>
<style>{globalStyles}</style>
<header style={{ borderBottom: '1px solid #1a1a1a', padding: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#050505' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<img src="/assets/imagenes/Icono-intro.jpeg" alt="NAYLA" style={{ width: '35px', height: '35px', borderRadius: '8px', objectFit: 'cover' }} />
<span style={{ fontSize: '0.7rem', padding: '4px 10px', border: '1px solid #fff', borderRadius: '100px', letterSpacing: '2px', fontWeight: 'bold' }}>LOGIC</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<select value={calidadExportacion} onChange={(e) => setCalidadExportacion(e.target.value)} style={{ backgroundColor: '#000', color: '#fff', border: '1px solid #404040', borderRadius: '8px', padding: '6px 10px', fontSize: '0.7rem', outline: 'none' }}>
<option value="480p">480p</option>
<option value="720p">720p</option>
<option value="1080p">1080p (FHD)</option>
<option value="4k">4K (UHD)</option>
</select>
<button onClick={handleDescargar} className="neon-btn nav-btn" style={{ padding: '8px 16px' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '6px' }}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
DESCARGAR
</button>
</div>
</header>
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<section style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '10px', backgroundColor: '#050505' }}>
<div ref={containerRef} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerLeave={handlePointerUp}
style={{ aspectRatio: canvasRatio, height: '42vh', maxHeight: '42vh', minHeight: '42vh', backgroundColor: '#0a0a0a', borderRadius: '16px', overflow: 'hidden', position: 'relative', border: '1px solid #1a1a1a', touchAction: 'none' }}>
{(videoResultadoUrl || videoTerminado) ? (
<>
{/* FIX AUDIO: removido muted */}
<video ref={videoRef} src={videoResultadoUrl || videoTerminado} loop playsInline onLoadedMetadata={(e) => setVideoMetadata({ width: e.currentTarget.videoWidth, height: e.currentTarget.videoHeight })} style={{ width: '100%', height: '100%', objectFit: 'contain', pointerEvents: 'none' }} />
{!videoResultadoUrl && rects.map((r) => (
<div key={r.id} onPointerDown={(e) => { e.stopPropagation(); if (!containerRef.current) return; const c = containerRef.current.getBoundingClientRect(); setDraggingInfo({ id: r.id, offsetX: (e.clientX - c.left) - r.x, offsetY: (e.clientY - c.top) - r.y }); }}
style={{ position: 'absolute', left: `${r.x}px`, top: `${r.y}px`, width: `${r.width}px`, height: `${r.height}px`, border: '1px solid #fff', backgroundColor: 'rgba(255,255,255,0.1)', pointerEvents: 'auto', cursor: 'move', borderRadius: '8px' }}>
<div onPointerDown={(e) => { e.stopPropagation(); removeRect(r.id); }} style={{ position: 'absolute', top: '-10px', right: '-10px', width: '20px', height: '20px', backgroundColor: '#fff', color: '#000', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', cursor: 'pointer', fontSize: '10px', fontWeight: 'bold', zIndex: 10 }}></div>
<div onPointerDown={(e) => { e.stopPropagation(); setResizingInfo({ id: r.id, corner: 'se' }); }} style={{ position: 'absolute', bottom: '-6px', right: '-6px', width: '12px', height: '12px', backgroundColor: '#fff', cursor: 'nwse-resize', zIndex: 10, borderRadius: '50%' }} />
</div>
))}
{currentRect && isDrawing && <div style={{ position: 'absolute', left: `${currentRect.x}px`, top: `${currentRect.y}px`, width: `${currentRect.width}px`, height: `${currentRect.height}px`, border: '1px dashed #fff', pointerEvents: 'none', borderRadius: '8px' }} />}
</>
) : (
<div style={{ display: 'flex', height: '100%', justifyContent: 'center', alignItems: 'center' }}>
<img src="/assets/imagenes/Icono-intro.jpeg" alt="NAYLA" style={{ width: '80px', height: '80px', borderRadius: '16px', opacity: 0.5, filter: 'grayscale(100%)' }} />
</div>
)}
</div>
</section>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 20px', alignItems: 'center', backgroundColor: '#000', borderBottom: '1px solid #1a1a1a' }}>
<span style={{ color: '#737373', fontSize: '0.75rem', fontFamily: 'monospace' }}>00:00:00</span>
<button onClick={togglePlay} style={{ background: 'none', border: 'none', color: '#fff', fontSize: '1.5rem', cursor: 'pointer', outline: 'none' }}>{isPlaying ? '⏸' : '▶'}</button>
<span style={{ color: '#737373', fontSize: '0.75rem', fontFamily: 'monospace' }}>00:00:00</span>
</div>
{/* TIMELINE HORIZONTAL */}
<section style={{ height: '140px', backgroundColor: '#050505', position: 'relative', borderBottom: '1px solid #1a1a1a', overflow: 'hidden', display: 'flex', flexDirection: 'column', padding: '10px 0' }} onClick={() => setClipSeleccionado(null)}>
<div style={{ position: 'absolute', left: hayClips ? '56px' : '50%', top: 0, bottom: 0, width: '2px', backgroundColor: '#fff', zIndex: 50, pointerEvents: 'none', boxShadow: '0 0 10px rgba(255,255,255,0.8)', transition: 'left 0.3s ease' }} />
<div className="timeline-track" ref={timelineRef}
style={{ paddingLeft: hayClips ? '8px' : 'calc(50% - 20px)', paddingRight: '50%', transition: 'padding-left 0.3s ease' }}
onClick={(e) => e.stopPropagation()}>
{/* FIX BOTÓN +: agregado e.stopPropagation() */}
<div className="neon-btn"
onClick={(e) => { e.stopPropagation(); setNavActiva('galeria'); setToolMessage(null); }}
style={{ width: '40px', height: '60px', minWidth: '40px', borderRadius: '10px', flexShrink: 0, marginRight: hayClips ? '6px' : '0', borderStyle: 'dashed', cursor: 'pointer', fontSize: '1.4rem', transition: 'margin 0.3s ease' }}>+</div>
{pistaVideo.map((clip) => (
<div key={clip.id}
onClick={(e) => { e.stopPropagation(); setClipSeleccionado(clip.id); }}
className={`clip-block ${clipSeleccionado === clip.id ? 'selected' : ''}`}>
{clip.tipo === 'video'
? <video src={clip.url} style={{ width: '100%', height: '100%', objectFit: 'cover' }} muted playsInline />
: <img src={clip.url} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
<span style={{ position: 'absolute', top: '4px', left: '6px', fontSize: '0.6rem', color: '#fff', fontWeight: 'bold', textShadow: '0 2px 4px #000', backgroundColor: 'rgba(0,0,0,0.5)', padding: '2px 4px', borderRadius: '4px' }}>{clip.etiqueta}</span>
{clipSeleccionado === clip.id && (
<div onClick={(e) => { e.stopPropagation(); quitarDelTimeline(clip.id); }}
style={{ position: 'absolute', top: '-8px', right: '-8px', width: '18px', height: '18px', backgroundColor: '#ff4444', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '10px', fontWeight: 'bold', cursor: 'pointer', zIndex: 20, color: '#fff' }}></div>
)}
</div>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', height: '35px', overflowX: 'auto', padding: '0 50%', gap: '2px', marginTop: '8px' }} onClick={(e) => e.stopPropagation()}>
{pistaAudio.map((clip) => (
<div key={clip.id} onClick={() => setClipSeleccionado(clip.id)} className="audio-block neon-btn" style={{ borderColor: clipSeleccionado === clip.id ? '#fff' : '#404040' }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginRight: '5px' }}><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
{clip.etiqueta}
</div>
))}
</div>
</section>
{/* PANELES */}
{navActiva && (
<div className="panel-container">
{toolMessage && <div style={{ textAlign: 'center', padding: '2rem', color: '#a3a3a3', fontSize: '1rem', letterSpacing: '2px' }}>{toolMessage}</div>}
{!toolMessage && navActiva === 'galeria' && (
<div style={{ maxHeight: '35vh', overflowY: 'auto' }}>
<div style={{ display: 'flex', gap: '10px', marginBottom: '1.5rem' }}>
<label className="neon-btn" style={{ flex: 1, padding: '1rem', borderStyle: 'dashed', borderRadius: '12px', fontSize: '0.75rem', cursor: 'pointer' }}>+ SUBIR VIDEO/FOTO<input type="file" multiple accept="video/*,image/*" onChange={(e) => handleSubirMultimedia(e, 'video')} style={{ display: 'none' }} /></label>
<label className="neon-btn" style={{ flex: 1, padding: '1rem', borderStyle: 'dashed', borderRadius: '12px', fontSize: '0.75rem', cursor: 'pointer' }}>+ SUBIR AUDIO<input type="file" multiple accept="audio/*" onChange={(e) => handleSubirMultimedia(e, 'audio')} style={{ display: 'none' }} /></label>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
{galeriaMultimedia.map(item => (
<div key={item.id} className="neon-btn" style={{ justifyContent: 'space-between', padding: '1rem', borderRadius: '16px', borderStyle: 'solid' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px', flex: 1 }}>
<span style={{ fontSize: '0.8rem', backgroundColor: '#262626', padding: '4px 8px', borderRadius: '6px', color: '#fff', fontWeight: 'bold' }}>{item.etiqueta}</span>
<input type="text" value={item.nombre} onChange={(e) => renombrarItem(item.id, e.target.value)} style={{ background: 'transparent', border: 'none', color: '#fff', outline: 'none', width: '100%' }} />
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button className="neon-btn nav-btn active" onClick={() => agregarAlTimeline(item)} style={{ padding: '6px 12px', fontSize: '0.65rem' }}>+ PISTA</button>
<button onClick={() => eliminarDeGaleria(item.id)} style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer', fontSize: '1rem' }}></button>
</div>
</div>
))}
</div>
</div>
)}
{!toolMessage && navActiva === 'marco' && (
<div style={{ maxHeight: '50vh', overflowY: 'auto' }}>
<p style={{ fontSize: '0.75rem', color: '#fff', fontWeight: 'bold', marginBottom: '1rem', letterSpacing: '1px' }}>MARCO CUBRIR MARCA DE AGUA</p>
<p style={{ fontSize: '0.65rem', color: '#737373', marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '1px' }}>Posición del marco</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px', marginBottom: '1.2rem' }}>
{POSICIONES.map(pos => (
<button key={pos} className={`marco-pos-btn ${marcoConfig.posicion === pos ? 'selected' : ''}`} onClick={() => setMarcoConfig({ ...marcoConfig, posicion: pos })}>
{ICONOS_POS[pos]}<br /><span style={{ fontSize: '0.5rem', opacity: 0.7 }}>{pos}</span>
</button>
))}
</div>
<p style={{ fontSize: '0.65rem', color: '#737373', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '1px' }}>Grosor: {marcoConfig.grosor}px</p>
<input type="range" min="20" max="200" value={marcoConfig.grosor} onChange={(e) => setMarcoConfig({ ...marcoConfig, grosor: parseInt(e.target.value) })} style={{ width: '100%', marginBottom: '1.2rem', accentColor: '#fff' }} />
<p style={{ fontSize: '0.65rem', color: '#737373', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '1px' }}>Color</p>
<div style={{ display: 'flex', gap: '10px', marginBottom: '1.2rem', alignItems: 'center' }}>
{['#ffffff', '#000000', '#1a1a1a', '#f5f5f5', '#e0e0e0'].map(color => (
<div key={color} onClick={() => setMarcoConfig({ ...marcoConfig, color })} style={{ width: '36px', height: '36px', borderRadius: '50%', backgroundColor: color, cursor: 'pointer', border: marcoConfig.color === color ? '3px solid #00ffcc' : '2px solid #333', flexShrink: 0 }} />
))}
<input type="color" value={marcoConfig.color} onChange={(e) => setMarcoConfig({ ...marcoConfig, color: e.target.value })} style={{ width: '36px', height: '36px', borderRadius: '50%', border: 'none', cursor: 'pointer' }} />
</div>
<div style={{ backgroundColor: '#0a0a0a', padding: '10px 14px', borderRadius: '10px', border: '1px solid #1a1a1a', marginBottom: '1.2rem' }}>
<span style={{ fontSize: '0.65rem', color: '#a3a3a3' }}>Ratio activo: <strong style={{ color: '#fff' }}>{canvasRatio}</strong> {galeriaMultimedia.filter(i => i.tipo === 'foto').length} fotos en bodega</span>
</div>
<button onClick={procesarImagenesConMarco} disabled={marcoProcesando} className="neon-btn nav-btn"
style={{ width: '100%', backgroundColor: marcoProcesando ? '#0a0a0a' : '#fff', color: marcoProcesando ? '#a3a3a3' : '#000', borderColor: marcoProcesando ? '#262626' : '#fff' }}>
{marcoProcesando ? 'PROCESANDO...' : '⚡ APLICAR A TODAS LAS FOTOS'}
</button>
{marcoImagenes.length > 0 && (
<div style={{ marginTop: '1.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<p style={{ fontSize: '0.75rem', color: '#00ffcc', fontWeight: 'bold', margin: 0 }}> {marcoImagenes.length} imágenes procesadas</p>
<button onClick={descargarTodasConMarco} className="neon-btn nav-btn" style={{ padding: '6px 14px', fontSize: '0.65rem' }}>DESCARGAR TODAS</button>
</div>
<div style={{ display: 'flex', gap: '10px', overflowX: 'auto', paddingBottom: '8px' }}>
{marcoImagenes.map((img, i) => (
<div key={i} style={{ flexShrink: 0, textAlign: 'center' }}>
<img src={img.procesada} style={{ width: '70px', height: '120px', objectFit: 'cover', borderRadius: '8px', border: '1px solid #333', display: 'block', marginBottom: '6px' }} />
<button onClick={() => descargarImagenConMarco(img.procesada, img.nombre)} style={{ background: 'none', border: '1px solid #333', color: '#a3a3a3', borderRadius: '6px', padding: '3px 6px', fontSize: '0.55rem', cursor: 'pointer' }}> DL</button>
</div>
))}
</div>
</div>
)}
</div>
)}
{!toolMessage && navActiva === 'herramientas' && (
<div style={{ maxHeight: '35vh', overflowY: 'auto' }}>
<p style={{ fontSize: '0.75rem', color: '#fff', fontWeight: 'bold', marginBottom: '1rem', letterSpacing: '1px' }}>SUPRESIÓN DE MARCA DE AGUA (DELOGO)</p>
<div style={{ backgroundColor: '#0a0a0a', padding: '1.5rem', borderRadius: '16px', border: '1px dashed #404040', marginBottom: '1rem' }}>
<p style={{ fontSize: '0.7rem', color: '#a3a3a3', marginBottom: '1rem' }}>1. Selecciona un video en la pista.<br />2. Dibuja un rectángulo blanco sobre el logo en el monitor.<br />3. Elige el motor de procesamiento.</p>
<div style={{ display: 'flex', gap: '10px' }}>
<button onClick={() => processVideo('local')} disabled={isProcessing} className="neon-btn nav-btn" style={{ flex: 1 }}>{isProcessing ? 'PROCESANDO...' : 'BORRAR LOGO (LOCAL)'}</button>
<button onClick={() => processVideo('nube')} disabled={isProcessing} className="neon-btn nav-btn" style={{ flex: 1 }}>{isProcessing ? 'PROCESANDO...' : 'BORRAR LOGO (APIS)'}</button>
</div>
</div>
</div>
)}
{!toolMessage && navActiva === 'script' && (
<div style={{ maxHeight: '35vh', overflowY: 'auto' }}>
<p style={{ fontSize: '0.75rem', color: '#fff', fontWeight: 'bold', marginBottom: '1rem', letterSpacing: '1px' }}>SCRIPT MANUAL</p>
<textarea value={codigoJsInput} onChange={(e) => setCodigoJsInput(e.target.value)} style={{ width: '100%', height: '80px', backgroundColor: '#0a0a0a', border: '1px solid #262626', borderRadius: '12px', color: '#00ffcc', padding: '1rem', fontFamily: 'monospace', outline: 'none' }} />
</div>
)}
{!toolMessage && navActiva === 'ia' && (
<div style={{ maxHeight: '35vh', overflowY: 'auto' }}>
<div style={{ textAlign: 'center', padding: '2rem', color: '#a3a3a3', fontSize: '1rem', letterSpacing: '2px' }}>SUPERVISOR IA (En desarrollo)</div>
</div>
)}
</div>
)}
<div className="toolbar-container">
{TOOLS.map((tool) => (
<button key={tool.id} className={`tool-btn ${navActiva === tool.id ? 'active' : ''}`} onClick={() => handleToolClick(tool)}>
<div className="tool-icon">{tool.icon}</div>
<span>{tool.nombre}</span>
</button>
))}
</div>
</div>
</div>
);
}
@@ -1,4 +1,5 @@
import { z } from "zod";
import { fontFamily, loadFont } from "@remotion/google-fonts/Inter";
import React, { useMemo } from "react";
import {
AbsoluteFill,
Sequence,
@@ -6,14 +7,16 @@ import {
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { CompositionProps } from "../../types/constants";
import { z } from "zod";
import { CompositionProps } from "../../../types/constants";
import { NextLogo } from "./NextLogo";
import { loadFont, fontFamily } from "@remotion/google-fonts/Inter";
import React, { useMemo } from "react";
import { Rings } from "./Rings";
import { TextFade } from "./TextFade";
loadFont();
loadFont("normal", {
subsets: ["latin"],
weights: ["400", "700"],
});
const container: React.CSSProperties = {
backgroundColor: "white",
+2 -2
View File
@@ -1,5 +1,4 @@
import { Composition } from "remotion";
import { Main } from "./MyComp/Main";
import {
COMP_NAME,
defaultMyCompProps,
@@ -7,7 +6,8 @@ import {
VIDEO_FPS,
VIDEO_HEIGHT,
VIDEO_WIDTH,
} from "../types/constants";
} from "../../types/constants";
import { Main } from "./MyComp/Main";
import { NextLogo } from "./MyComp/NextLogo";
export const RemotionRoot: React.FC = () => {
+17 -6
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -12,10 +16,17 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUnusedLocals": true
"jsx": "react-jsx",
"noUnusedLocals": true,
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"remotion.config.ts"
]
}