#
tokens: 4337/50000 1/58 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 3. Use http://codebase.md/gyoridavid/short-video-maker?lines=true&page={x} to view the full context.

# Directory Structure

```
├── __mocks__
│   └── pexels-response.json
├── .dockerignore
├── .editorconfig
├── .env.example
├── .gitignore
├── .prettierrc
├── CONTRIBUTING.md
├── docker-compose.yml
├── eslint.config.mjs
├── LICENSE
├── main-cuda.Dockerfile
├── main-tiny.Dockerfile
├── main.Dockerfile
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── postcss.config.mjs
├── README.md
├── remotion.config.ts
├── rest.http
├── src
│   ├── components
│   │   ├── root
│   │   │   ├── index.ts
│   │   │   └── Root.tsx
│   │   ├── types.ts
│   │   ├── utils.ts
│   │   └── videos
│   │       ├── LandscapeVideo.tsx
│   │       ├── PortraitVideo.tsx
│   │       └── Test.tsx
│   ├── config.ts
│   ├── index.ts
│   ├── logger.ts
│   ├── scripts
│   │   ├── install.ts
│   │   └── normalizeMusic.ts
│   ├── server
│   │   ├── routers
│   │   │   ├── mcp.ts
│   │   │   └── rest.ts
│   │   ├── server.ts
│   │   └── validator.ts
│   ├── short-creator
│   │   ├── libraries
│   │   │   ├── FFmpeg.ts
│   │   │   ├── Kokoro.ts
│   │   │   ├── Pexels.test.ts
│   │   │   ├── Pexels.ts
│   │   │   ├── Remotion.ts
│   │   │   └── Whisper.ts
│   │   ├── music.ts
│   │   ├── ShortCreator.test.ts
│   │   └── ShortCreator.ts
│   ├── types
│   │   └── shorts.ts
│   └── ui
│       ├── App.tsx
│       ├── components
│       │   └── Layout.tsx
│       ├── index.html
│       ├── index.tsx
│       ├── pages
│       │   ├── VideoCreator.tsx
│       │   ├── VideoDetails.tsx
│       │   └── VideoList.tsx
│       ├── public
│       │   └── index.html
│       └── styles
│           └── index.css
├── static
│   └── music
│       ├── Aurora on the Boulevard - National Sweetheart.mp3
│       ├── Baby Animals Playing - Joel Cummins.mp3
│       ├── Banjo Doops - Joel Cummins.mp3
│       ├── Buckle Up - Jeremy Korpas.mp3
│       ├── Cafecito por la Manana - Cumbia Deli.mp3
│       ├── Champion - Telecasted.mp3
│       ├── Crystaline - Quincas Moreira.mp3
│       ├── Curse of the Witches - Jimena Contreras.mp3
│       ├── Delayed Baggage - Ryan Stasik.mp3
│       ├── Final Soliloquy - Asher Fulero.mp3
│       ├── Heartbeat Of The Wind - Asher Fulero.mp3
│       ├── Honey, I Dismembered The Kids - Ezra Lipp.mp3
│       ├── Hopeful - Nat Keefe.mp3
│       ├── Hopeful Freedom - Asher Fulero.mp3
│       ├── Hopeless - Jimena Contreras.mp3
│       ├── Jetski - Telecasted.mp3
│       ├── Like It Loud - Dyalla.mp3
│       ├── Name The Time And Place - Telecasted.mp3
│       ├── Night Hunt - Jimena Contreras.mp3
│       ├── No.2 Remembering Her - Esther Abrami.mp3
│       ├── Oh Please - Telecasted.mp3
│       ├── On The Hunt - Andrew Langdon.mp3
│       ├── Organic Guitar House - Dyalla.mp3
│       ├── Phantom - Density & Time.mp3
│       ├── README.md
│       ├── Restless Heart - Jimena Contreras.mp3
│       ├── Seagull - Telecasted.mp3
│       ├── Sinister - Anno Domini Beats.mp3
│       ├── Sly Sky - Telecasted.mp3
│       ├── Touch - Anno Domini Beats.mp3
│       ├── Traversing - Godmode.mp3
│       └── Twin Engines - Jeremy Korpas.mp3
├── tailwind.config.js
├── tsconfig.build.json
├── tsconfig.json
├── vite.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/ui/pages/VideoCreator.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useState, useEffect } from "react";
  2 | import axios from "axios";
  3 | import { useNavigate } from "react-router-dom";
  4 | import {
  5 |   Box,
  6 |   Button,
  7 |   TextField,
  8 |   Typography,
  9 |   Paper,
 10 |   Grid,
 11 |   FormControl,
 12 |   InputLabel,
 13 |   Select,
 14 |   MenuItem,
 15 |   CircularProgress,
 16 |   Alert,
 17 |   IconButton,
 18 |   Divider,
 19 |   InputAdornment,
 20 | } from "@mui/material";
 21 | import AddIcon from "@mui/icons-material/Add";
 22 | import DeleteIcon from "@mui/icons-material/Delete";
 23 | import {
 24 |   SceneInput,
 25 |   RenderConfig,
 26 |   MusicMoodEnum,
 27 |   CaptionPositionEnum,
 28 |   VoiceEnum,
 29 |   OrientationEnum,
 30 |   MusicVolumeEnum,
 31 | } from "../../types/shorts";
 32 | 
 33 | interface SceneFormData {
 34 |   text: string;
 35 |   searchTerms: string; // Changed to string
 36 | }
 37 | 
 38 | const VideoCreator: React.FC = () => {
 39 |   const navigate = useNavigate();
 40 |   const [scenes, setScenes] = useState<SceneFormData[]>([
 41 |     { text: "", searchTerms: "" },
 42 |   ]);
 43 |   const [config, setConfig] = useState<RenderConfig>({
 44 |     paddingBack: 1500,
 45 |     music: MusicMoodEnum.chill,
 46 |     captionPosition: CaptionPositionEnum.bottom,
 47 |     captionBackgroundColor: "blue",
 48 |     voice: VoiceEnum.af_heart,
 49 |     orientation: OrientationEnum.portrait,
 50 |     musicVolume: MusicVolumeEnum.high,
 51 |   });
 52 | 
 53 |   const [loading, setLoading] = useState(false);
 54 |   const [error, setError] = useState<string | null>(null);
 55 |   const [voices, setVoices] = useState<VoiceEnum[]>([]);
 56 |   const [musicTags, setMusicTags] = useState<MusicMoodEnum[]>([]);
 57 |   const [loadingOptions, setLoadingOptions] = useState(true);
 58 | 
 59 |   useEffect(() => {
 60 |     const fetchOptions = async () => {
 61 |       try {
 62 |         const [voicesResponse, musicResponse] = await Promise.all([
 63 |           axios.get("/api/voices"),
 64 |           axios.get("/api/music-tags"),
 65 |         ]);
 66 | 
 67 |         setVoices(voicesResponse.data);
 68 |         setMusicTags(musicResponse.data);
 69 |       } catch (err) {
 70 |         console.error("Failed to fetch options:", err);
 71 |         setError(
 72 |           "Failed to load voices and music options. Please refresh the page.",
 73 |         );
 74 |       } finally {
 75 |         setLoadingOptions(false);
 76 |       }
 77 |     };
 78 | 
 79 |     fetchOptions();
 80 |   }, []);
 81 | 
 82 |   const handleAddScene = () => {
 83 |     setScenes([...scenes, { text: "", searchTerms: "" }]);
 84 |   };
 85 | 
 86 |   const handleRemoveScene = (index: number) => {
 87 |     if (scenes.length > 1) {
 88 |       const newScenes = [...scenes];
 89 |       newScenes.splice(index, 1);
 90 |       setScenes(newScenes);
 91 |     }
 92 |   };
 93 | 
 94 |   const handleSceneChange = (
 95 |     index: number,
 96 |     field: keyof SceneFormData,
 97 |     value: string,
 98 |   ) => {
 99 |     const newScenes = [...scenes];
100 |     newScenes[index] = { ...newScenes[index], [field]: value };
101 |     setScenes(newScenes);
102 |   };
103 | 
104 |   const handleConfigChange = (field: keyof RenderConfig, value: any) => {
105 |     setConfig({ ...config, [field]: value });
106 |   };
107 | 
108 |   const handleSubmit = async (e: React.FormEvent) => {
109 |     e.preventDefault();
110 |     setLoading(true);
111 |     setError(null);
112 | 
113 |     try {
114 |       // Convert scenes to the expected API format
115 |       const apiScenes: SceneInput[] = scenes.map((scene) => ({
116 |         text: scene.text,
117 |         searchTerms: scene.searchTerms
118 |           .split(",")
119 |           .map((term) => term.trim())
120 |           .filter((term) => term.length > 0),
121 |       }));
122 | 
123 |       const response = await axios.post("/api/short-video", {
124 |         scenes: apiScenes,
125 |         config,
126 |       });
127 | 
128 |       navigate(`/video/${response.data.videoId}`);
129 |     } catch (err) {
130 |       setError("Failed to create video. Please try again.");
131 |       console.error(err);
132 |     } finally {
133 |       setLoading(false);
134 |     }
135 |   };
136 | 
137 |   if (loadingOptions) {
138 |     return (
139 |       <Box
140 |         display="flex"
141 |         justifyContent="center"
142 |         alignItems="center"
143 |         height="80vh"
144 |       >
145 |         <CircularProgress />
146 |       </Box>
147 |     );
148 |   }
149 | 
150 |   return (
151 |     <Box maxWidth="md" mx="auto" py={4}>
152 |       <Typography variant="h4" component="h1" gutterBottom>
153 |         Create New Video
154 |       </Typography>
155 | 
156 |       {error && (
157 |         <Alert severity="error" sx={{ mb: 3 }}>
158 |           {error}
159 |         </Alert>
160 |       )}
161 | 
162 |       <form onSubmit={handleSubmit}>
163 |         <Typography variant="h5" component="h2" gutterBottom>
164 |           Scenes
165 |         </Typography>
166 | 
167 |         {scenes.map((scene, index) => (
168 |           <Paper key={index} sx={{ p: 3, mb: 3 }}>
169 |             <Box
170 |               display="flex"
171 |               justifyContent="space-between"
172 |               alignItems="center"
173 |               mb={2}
174 |             >
175 |               <Typography variant="h6">Scene {index + 1}</Typography>
176 |               {scenes.length > 1 && (
177 |                 <IconButton
178 |                   onClick={() => handleRemoveScene(index)}
179 |                   color="error"
180 |                   size="small"
181 |                 >
182 |                   <DeleteIcon />
183 |                 </IconButton>
184 |               )}
185 |             </Box>
186 | 
187 |             <Grid container spacing={3}>
188 |               <Grid item xs={12}>
189 |                 <TextField
190 |                   fullWidth
191 |                   label="Text"
192 |                   multiline
193 |                   rows={4}
194 |                   value={scene.text}
195 |                   onChange={(e) =>
196 |                     handleSceneChange(index, "text", e.target.value)
197 |                   }
198 |                   required
199 |                 />
200 |               </Grid>
201 | 
202 |               <Grid item xs={12}>
203 |                 <TextField
204 |                   fullWidth
205 |                   label="Search Terms (comma-separated)"
206 |                   value={scene.searchTerms}
207 |                   onChange={(e) =>
208 |                     handleSceneChange(index, "searchTerms", e.target.value)
209 |                   }
210 |                   helperText="Enter keywords for background video, separated by commas"
211 |                   required
212 |                 />
213 |               </Grid>
214 |             </Grid>
215 |           </Paper>
216 |         ))}
217 | 
218 |         <Box display="flex" justifyContent="center" mb={4}>
219 |           <Button
220 |             variant="outlined"
221 |             startIcon={<AddIcon />}
222 |             onClick={handleAddScene}
223 |           >
224 |             Add Scene
225 |           </Button>
226 |         </Box>
227 | 
228 |         <Divider sx={{ mb: 4 }} />
229 | 
230 |         <Typography variant="h5" component="h2" gutterBottom>
231 |           Video Configuration
232 |         </Typography>
233 | 
234 |         <Paper sx={{ p: 3, mb: 3 }}>
235 |           <Grid container spacing={3}>
236 |             <Grid item xs={12} sm={6}>
237 |               <TextField
238 |                 fullWidth
239 |                 type="number"
240 |                 label="End Screen Padding (ms)"
241 |                 value={config.paddingBack}
242 |                 onChange={(e) =>
243 |                   handleConfigChange("paddingBack", parseInt(e.target.value))
244 |                 }
245 |                 InputProps={{
246 |                   endAdornment: (
247 |                     <InputAdornment position="end">ms</InputAdornment>
248 |                   ),
249 |                 }}
250 |                 helperText="Duration to keep playing after narration ends"
251 |                 required
252 |               />
253 |             </Grid>
254 | 
255 |             <Grid item xs={12} sm={6}>
256 |               <FormControl fullWidth>
257 |                 <InputLabel>Music Mood</InputLabel>
258 |                 <Select
259 |                   value={config.music}
260 |                   onChange={(e) => handleConfigChange("music", e.target.value)}
261 |                   label="Music Mood"
262 |                   required
263 |                 >
264 |                   {Object.values(MusicMoodEnum).map((tag) => (
265 |                     <MenuItem key={tag} value={tag}>
266 |                       {tag}
267 |                     </MenuItem>
268 |                   ))}
269 |                 </Select>
270 |               </FormControl>
271 |             </Grid>
272 | 
273 |             <Grid item xs={12} sm={6}>
274 |               <FormControl fullWidth>
275 |                 <InputLabel>Caption Position</InputLabel>
276 |                 <Select
277 |                   value={config.captionPosition}
278 |                   onChange={(e) =>
279 |                     handleConfigChange("captionPosition", e.target.value)
280 |                   }
281 |                   label="Caption Position"
282 |                   required
283 |                 >
284 |                   {Object.values(CaptionPositionEnum).map((position) => (
285 |                     <MenuItem key={position} value={position}>
286 |                       {position}
287 |                     </MenuItem>
288 |                   ))}
289 |                 </Select>
290 |               </FormControl>
291 |             </Grid>
292 | 
293 |             <Grid item xs={12} sm={6}>
294 |               <TextField
295 |                 fullWidth
296 |                 label="Caption Background Color"
297 |                 value={config.captionBackgroundColor}
298 |                 onChange={(e) =>
299 |                   handleConfigChange("captionBackgroundColor", e.target.value)
300 |                 }
301 |                 helperText="Any valid CSS color (name, hex, rgba)"
302 |                 required
303 |               />
304 |             </Grid>
305 | 
306 |             <Grid item xs={12} sm={6}>
307 |               <FormControl fullWidth>
308 |                 <InputLabel>Default Voice</InputLabel>
309 |                 <Select
310 |                   value={config.voice}
311 |                   onChange={(e) => handleConfigChange("voice", e.target.value)}
312 |                   label="Default Voice"
313 |                   required
314 |                 >
315 |                   {Object.values(VoiceEnum).map((voice) => (
316 |                     <MenuItem key={voice} value={voice}>
317 |                       {voice}
318 |                     </MenuItem>
319 |                   ))}
320 |                 </Select>
321 |               </FormControl>
322 |             </Grid>
323 | 
324 |             <Grid item xs={12} sm={6}>
325 |               <FormControl fullWidth>
326 |                 <InputLabel>Orientation</InputLabel>
327 |                 <Select
328 |                   value={config.orientation}
329 |                   onChange={(e) =>
330 |                     handleConfigChange("orientation", e.target.value)
331 |                   }
332 |                   label="Orientation"
333 |                   required
334 |                 >
335 |                   {Object.values(OrientationEnum).map((orientation) => (
336 |                     <MenuItem key={orientation} value={orientation}>
337 |                       {orientation}
338 |                     </MenuItem>
339 |                   ))}
340 |                 </Select>
341 |               </FormControl>
342 |             </Grid>
343 | 
344 |             <Grid item xs={12} sm={6}>
345 |               <FormControl fullWidth>
346 |                 <InputLabel>Volume of the background audio</InputLabel>
347 |                 <Select
348 |                   value={config.musicVolume}
349 |                   onChange={(e) =>
350 |                     handleConfigChange("musicVolume", e.target.value)
351 |                   }
352 |                   label="Volume of the background audio"
353 |                   required
354 |                 >
355 |                   {Object.values(MusicVolumeEnum).map((voice) => (
356 |                     <MenuItem key={voice} value={voice}>
357 |                       {voice}
358 |                     </MenuItem>
359 |                   ))}
360 |                 </Select>
361 |               </FormControl>
362 |             </Grid>
363 |           </Grid>
364 |         </Paper>
365 | 
366 |         <Box display="flex" justifyContent="center">
367 |           <Button
368 |             type="submit"
369 |             variant="contained"
370 |             color="primary"
371 |             size="large"
372 |             disabled={loading}
373 |             sx={{ minWidth: 200 }}
374 |           >
375 |             {loading ? (
376 |               <CircularProgress size={24} color="inherit" />
377 |             ) : (
378 |               "Create Video"
379 |             )}
380 |           </Button>
381 |         </Box>
382 |       </form>
383 |     </Box>
384 |   );
385 | };
386 | 
387 | export default VideoCreator;
388 | 
```
Page 2/3FirstPrevNextLast