dynamic assests and cutscences -- not tested
Browse files- App/Editor/Schema.py +13 -0
- App/Worker.py +21 -1
- Remotion-app/src/HelloWorld/Assets/AudioSequences.json +1 -0
- Remotion-app/src/HelloWorld/{Transcription.json → Assets/TextSequences.json} +0 -0
- Remotion-app/src/HelloWorld/Assets/VideoSequences.json +1 -0
- Remotion-app/src/HelloWorld/AudioStream.jsx +27 -0
- Remotion-app/src/HelloWorld/{Subtitle.jsx → TextStream.jsx} +24 -16
- Remotion-app/src/HelloWorld/VideoStream.jsx +27 -0
- Remotion-app/src/HelloWorld/constants.js +3 -1
- Remotion-app/src/HelloWorld/index.jsx +8 -42
- Remotion-app/src/Root.jsx +2 -5
App/Editor/Schema.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
from typing import List, Optional
|
2 |
from pydantic import BaseModel, HttpUrl
|
|
|
3 |
|
4 |
|
5 |
class LinkInfo(BaseModel):
|
@@ -7,8 +8,20 @@ class LinkInfo(BaseModel):
|
|
7 |
link: HttpUrl
|
8 |
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
class EditorRequest(BaseModel):
|
11 |
links: Optional[List[LinkInfo]] # List of LinkInfo objects
|
|
|
12 |
script: str
|
13 |
|
14 |
|
|
|
1 |
from typing import List, Optional
|
2 |
from pydantic import BaseModel, HttpUrl
|
3 |
+
from pydantic import validator
|
4 |
|
5 |
|
6 |
class LinkInfo(BaseModel):
|
|
|
8 |
link: HttpUrl
|
9 |
|
10 |
|
11 |
+
class Assets(BaseModel):
|
12 |
+
type: str
|
13 |
+
sequence: List[dict]
|
14 |
+
|
15 |
+
@validator("type")
|
16 |
+
def valid_type(cls, v):
|
17 |
+
if v not in ["video", "audio", "text"]:
|
18 |
+
raise ValueError("Invalid asset type")
|
19 |
+
return v
|
20 |
+
|
21 |
+
|
22 |
class EditorRequest(BaseModel):
|
23 |
links: Optional[List[LinkInfo]] # List of LinkInfo objects
|
24 |
+
assets: List[Assets]
|
25 |
script: str
|
26 |
|
27 |
|
App/Worker.py
CHANGED
@@ -4,9 +4,11 @@ import uuid
|
|
4 |
from urllib.parse import urlparse
|
5 |
from App import celery_config, bot
|
6 |
from typing import List
|
7 |
-
from App.Editor.Schema import EditorRequest, LinkInfo
|
8 |
from celery.signals import worker_process_init
|
9 |
from asgiref.sync import async_to_sync
|
|
|
|
|
10 |
|
11 |
celery = Celery()
|
12 |
celery.config_from_object(celery_config)
|
@@ -21,6 +23,22 @@ def worker_process_init_handler(**kwargs):
|
|
21 |
bot.start()
|
22 |
|
23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
def create_symlink(source_dir, target_dir, symlink_name):
|
25 |
source_path = os.path.join(source_dir, symlink_name)
|
26 |
target_path = os.path.join(target_dir, symlink_name)
|
@@ -83,10 +101,12 @@ def celery_task(video_task: EditorRequest):
|
|
83 |
project_id = str(uuid.uuid4())
|
84 |
temp_dir = f"/tmp/{project_id}"
|
85 |
output_dir = f"/tmp/{project_id}/out/video.mp4"
|
|
|
86 |
|
87 |
chain(
|
88 |
copy_remotion_app.si(remotion_app_dir, temp_dir),
|
89 |
install_dependencies.si(temp_dir),
|
|
|
90 |
download_assets.si(video_task.links, temp_dir) if video_task.links else None,
|
91 |
render_video.si(temp_dir, output_dir),
|
92 |
cleanup_temp_directory.si(temp_dir, output_dir),
|
|
|
4 |
from urllib.parse import urlparse
|
5 |
from App import celery_config, bot
|
6 |
from typing import List
|
7 |
+
from App.Editor.Schema import EditorRequest, LinkInfo, Assets
|
8 |
from celery.signals import worker_process_init
|
9 |
from asgiref.sync import async_to_sync
|
10 |
+
import json
|
11 |
+
import os
|
12 |
|
13 |
celery = Celery()
|
14 |
celery.config_from_object(celery_config)
|
|
|
23 |
bot.start()
|
24 |
|
25 |
|
26 |
+
@celery.task
|
27 |
+
def create_json_file(assets: List[Assets], asset_dir: str):
|
28 |
+
for asset in assets:
|
29 |
+
filename = f"{asset.type}Sequences.json"
|
30 |
+
filename = filename.capitalize()
|
31 |
+
# Convert dictionary to JSON string
|
32 |
+
json_string = json.dumps(asset.sequence)
|
33 |
+
|
34 |
+
# Create directory if it doesn't exist
|
35 |
+
os.makedirs(asset_dir, exist_ok=True)
|
36 |
+
|
37 |
+
# Write JSON string to file
|
38 |
+
with open(os.path.join(asset_dir, filename), "w") as f:
|
39 |
+
f.write(json_string)
|
40 |
+
|
41 |
+
|
42 |
def create_symlink(source_dir, target_dir, symlink_name):
|
43 |
source_path = os.path.join(source_dir, symlink_name)
|
44 |
target_path = os.path.join(target_dir, symlink_name)
|
|
|
101 |
project_id = str(uuid.uuid4())
|
102 |
temp_dir = f"/tmp/{project_id}"
|
103 |
output_dir = f"/tmp/{project_id}/out/video.mp4"
|
104 |
+
assets_dir = os.path.join(temp_dir, "src/Assets")
|
105 |
|
106 |
chain(
|
107 |
copy_remotion_app.si(remotion_app_dir, temp_dir),
|
108 |
install_dependencies.si(temp_dir),
|
109 |
+
create_json_file.si(video_task.assets, assets_dir),
|
110 |
download_assets.si(video_task.links, temp_dir) if video_task.links else None,
|
111 |
render_video.si(temp_dir, output_dir),
|
112 |
cleanup_temp_directory.si(temp_dir, output_dir),
|
Remotion-app/src/HelloWorld/Assets/AudioSequences.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[]
|
Remotion-app/src/HelloWorld/{Transcription.json → Assets/TextSequences.json}
RENAMED
File without changes
|
Remotion-app/src/HelloWorld/Assets/VideoSequences.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[]
|
Remotion-app/src/HelloWorld/AudioStream.jsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Series} from 'remotion';
|
2 |
+
import React from 'react';
|
3 |
+
import {staticFile, useVideoConfig} from 'remotion';
|
4 |
+
import audioSequences from './Assets/AudioSequences.json';
|
5 |
+
export default function AudioStream() {
|
6 |
+
const {fps} = useVideoConfig();
|
7 |
+
return (
|
8 |
+
<Series
|
9 |
+
style={{
|
10 |
+
color: 'white',
|
11 |
+
position: 'absolute',
|
12 |
+
zIndex: 0,
|
13 |
+
}}
|
14 |
+
>
|
15 |
+
{audioSequences.map((entry, index) => {
|
16 |
+
return (
|
17 |
+
<Series.Sequence
|
18 |
+
from={fps * entry.start}
|
19 |
+
durationInFrames={fps * (entry.end - entry.start)}
|
20 |
+
>
|
21 |
+
<Audio {...entry.props} src={staticFile(entry.name)} />
|
22 |
+
</Series.Sequence>
|
23 |
+
);
|
24 |
+
})}
|
25 |
+
</Series>
|
26 |
+
);
|
27 |
+
}
|
Remotion-app/src/HelloWorld/{Subtitle.jsx → TextStream.jsx}
RENAMED
@@ -1,24 +1,23 @@
|
|
1 |
import React, {createElement} from 'react';
|
2 |
import {useVideoConfig} from 'remotion';
|
3 |
-
import
|
4 |
-
import transcriptData from './
|
5 |
-
|
6 |
import {TransitionSeries} from '@remotion/transitions';
|
7 |
|
8 |
const codeStyle = (index) => {
|
9 |
return {
|
10 |
color: 'white',
|
11 |
position: 'absolute',
|
12 |
-
backgroundColor: 'red',
|
13 |
top: '50%',
|
14 |
-
|
15 |
transform: 'translate(-50%, -50%)',
|
16 |
left: '50%',
|
17 |
};
|
18 |
};
|
19 |
|
20 |
const subtitle = {
|
21 |
-
fontFamily:
|
22 |
fontSize: 120,
|
23 |
textAlign: 'center',
|
24 |
display: 'relative',
|
@@ -26,22 +25,31 @@ const subtitle = {
|
|
26 |
width: '100%',
|
27 |
};
|
28 |
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
const {fps} = useVideoConfig();
|
31 |
|
32 |
return (
|
33 |
-
<div style={
|
34 |
<TransitionSeries>
|
35 |
{transcriptData.map((entry, index) => {
|
36 |
return (
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
{
|
43 |
-
|
44 |
-
|
|
|
|
|
45 |
);
|
46 |
})}
|
47 |
</TransitionSeries>
|
|
|
1 |
import React, {createElement} from 'react';
|
2 |
import {useVideoConfig} from 'remotion';
|
3 |
+
import * as Fonts from '@remotion/google-fonts';
|
4 |
+
import transcriptData from './Assets/TextSequences.json';
|
5 |
+
import {FONT_FAMILY} from './constants';
|
6 |
import {TransitionSeries} from '@remotion/transitions';
|
7 |
|
8 |
const codeStyle = (index) => {
|
9 |
return {
|
10 |
color: 'white',
|
11 |
position: 'absolute',
|
|
|
12 |
top: '50%',
|
13 |
+
zIndex: 50,
|
14 |
transform: 'translate(-50%, -50%)',
|
15 |
left: '50%',
|
16 |
};
|
17 |
};
|
18 |
|
19 |
const subtitle = {
|
20 |
+
fontFamily: FONT_FAMILY,
|
21 |
fontSize: 120,
|
22 |
textAlign: 'center',
|
23 |
display: 'relative',
|
|
|
25 |
width: '100%',
|
26 |
};
|
27 |
|
28 |
+
Fonts.getAvailableFonts()
|
29 |
+
.filter((font) => {
|
30 |
+
return font.fontFamily === FONT_FAMILY;
|
31 |
+
})[0]
|
32 |
+
.load()
|
33 |
+
.then((font) => font.loadFont());
|
34 |
+
|
35 |
+
export const TextStream = () => {
|
36 |
const {fps} = useVideoConfig();
|
37 |
|
38 |
return (
|
39 |
+
<div style={{...temp, ...{}}}>
|
40 |
<TransitionSeries>
|
41 |
{transcriptData.map((entry, index) => {
|
42 |
return (
|
43 |
+
<>
|
44 |
+
<TransitionSeries.Sequence
|
45 |
+
from={entry.start * fps}
|
46 |
+
durationInFrames={fps * (entry.end - entry.start)}
|
47 |
+
>
|
48 |
+
<Letter index={index} color="#0b84f3">
|
49 |
+
{entry.text}
|
50 |
+
</Letter>
|
51 |
+
</TransitionSeries.Sequence>
|
52 |
+
</>
|
53 |
);
|
54 |
})}
|
55 |
</TransitionSeries>
|
Remotion-app/src/HelloWorld/VideoStream.jsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {Series} from 'remotion';
|
2 |
+
import React from 'react';
|
3 |
+
import {Video, staticFile, useVideoConfig} from 'remotion';
|
4 |
+
import videoSequences from './Assets/VideoSequences.json';
|
5 |
+
export default function VideoStream() {
|
6 |
+
const {fps} = useVideoConfig();
|
7 |
+
return (
|
8 |
+
<Series
|
9 |
+
style={{
|
10 |
+
color: 'white',
|
11 |
+
position: 'absolute',
|
12 |
+
zIndex: 0,
|
13 |
+
}}
|
14 |
+
>
|
15 |
+
{videoSequences.map((entry, index) => {
|
16 |
+
return (
|
17 |
+
<Series.Sequence
|
18 |
+
from={fps * entry.start}
|
19 |
+
durationInFrames={fps * (entry.end - entry.start)}
|
20 |
+
>
|
21 |
+
<Video {...entry.props} src={staticFile(entry.name)} />
|
22 |
+
</Series.Sequence>
|
23 |
+
);
|
24 |
+
})}
|
25 |
+
</Series>
|
26 |
+
);
|
27 |
+
}
|
Remotion-app/src/HelloWorld/constants.js
CHANGED
@@ -3,4 +3,6 @@
|
|
3 |
export const COLOR_1 = '#86A8E7';
|
4 |
export const COLOR_2 = '#91EAE4';
|
5 |
|
6 |
-
export const FONT_FAMILY = '
|
|
|
|
|
|
3 |
export const COLOR_1 = '#86A8E7';
|
4 |
export const COLOR_2 = '#91EAE4';
|
5 |
|
6 |
+
export const FONT_FAMILY = 'Bungee';
|
7 |
+
export const FPS = 30;
|
8 |
+
export const DURATION = 30 * FPS; // 30 seconds
|
Remotion-app/src/HelloWorld/index.jsx
CHANGED
@@ -1,48 +1,14 @@
|
|
1 |
-
import {
|
2 |
-
import
|
3 |
-
|
4 |
-
|
5 |
-
useCurrentFrame,
|
6 |
-
useVideoConfig,
|
7 |
-
Audio,
|
8 |
-
staticFile,
|
9 |
-
Video,
|
10 |
-
} from 'remotion';
|
11 |
-
import {preloadAudio, resolveRedirect} from '@remotion/preload';
|
12 |
-
|
13 |
-
export const HelloWorld = ({titleText, titleColor}) => {
|
14 |
-
const frame = useCurrentFrame();
|
15 |
-
const {fps} = useVideoConfig();
|
16 |
-
const scale = spring({
|
17 |
-
fps,
|
18 |
-
frame,
|
19 |
-
config: {
|
20 |
-
damping: 200,
|
21 |
-
mass: 0.5,
|
22 |
-
stiffness: 200,
|
23 |
-
overshootClamping: false,
|
24 |
-
restDisplacementThreshold: 0.01,
|
25 |
-
restSpeedThreshold: 0.01,
|
26 |
-
},
|
27 |
-
});
|
28 |
|
|
|
29 |
return (
|
30 |
<AbsoluteFill style={{position: 'relative', backgroundColor: 'black'}}>
|
31 |
-
<
|
32 |
-
|
33 |
-
|
34 |
-
// src={'https://yakova-streamer.hf.space/download/20707'}
|
35 |
-
/>
|
36 |
-
|
37 |
-
{/* <Video
|
38 |
-
src={
|
39 |
-
'https://player.vimeo.com/external/514185553.hd.mp4?s=33cb766901019185385a757eab89a9fd0d50d0c0&profile_id=172&oauth2_token_id=57447761'
|
40 |
-
}
|
41 |
-
/> */}
|
42 |
-
{/* <Audio src={'https://yakova-streamer.hf.space/download/20711'} /> */}
|
43 |
-
{/* <img src={''} style={{transform: `scale(${scale})`}} /> */}
|
44 |
-
|
45 |
-
<Subtitle />
|
46 |
</AbsoluteFill>
|
47 |
);
|
48 |
};
|
|
|
1 |
+
import {AbsoluteFill} from 'remotion';
|
2 |
+
import VideoStream from './VideoStream';
|
3 |
+
import TextStream from './TextStream';
|
4 |
+
import AudioStream from './AudioStream';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
+
export const HelloWorld = () => {
|
7 |
return (
|
8 |
<AbsoluteFill style={{position: 'relative', backgroundColor: 'black'}}>
|
9 |
+
<TextStream />
|
10 |
+
<VideoStream />
|
11 |
+
<AudioStream />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
</AbsoluteFill>
|
13 |
);
|
14 |
};
|
Remotion-app/src/Root.jsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import {Composition} from 'remotion';
|
2 |
import {HelloWorld} from './HelloWorld';
|
|
|
3 |
|
4 |
export const RemotionRoot = () => {
|
5 |
return (
|
@@ -7,14 +8,10 @@ export const RemotionRoot = () => {
|
|
7 |
<Composition
|
8 |
id="HelloWorld"
|
9 |
component={HelloWorld}
|
10 |
-
durationInFrames={
|
11 |
fps={30}
|
12 |
height={1920}
|
13 |
width={1080}
|
14 |
-
defaultProps={{
|
15 |
-
titleText: 'Welcome to Remotion',
|
16 |
-
titleColor: 'black',
|
17 |
-
}}
|
18 |
/>
|
19 |
</>
|
20 |
);
|
|
|
1 |
import {Composition} from 'remotion';
|
2 |
import {HelloWorld} from './HelloWorld';
|
3 |
+
import {DURATION} from './HelloWorld/constants';
|
4 |
|
5 |
export const RemotionRoot = () => {
|
6 |
return (
|
|
|
8 |
<Composition
|
9 |
id="HelloWorld"
|
10 |
component={HelloWorld}
|
11 |
+
durationInFrames={DURATION}
|
12 |
fps={30}
|
13 |
height={1920}
|
14 |
width={1080}
|
|
|
|
|
|
|
|
|
15 |
/>
|
16 |
</>
|
17 |
);
|