Adityasv99 commited on
Commit
47b4017
·
0 Parent(s):

Clean repository for Hugging Face deployment

Browse files
.gitignore ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual Environment
2
+ venv/
3
+
4
+ # Python cache
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.pyd
9
+
10
+ # Environment variables
11
+ .env
12
+
13
+ # Build folders
14
+ dist/
15
+ build/
16
+
17
+ # Project generated files
18
+ assets/uploads/
19
+ assets/outputs/
20
+ !assets/uploads/.gitkeep
21
+ !assets/outputs/.gitkeep
22
+
23
+ # OS files
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # IDE settings
28
+ .vscode/
README.md ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLIPFORGE
2
+
3
+ 🚀 CLPFORGE
4
+ AI-Powered Content Creation Engine
5
+
6
+ CLPFORGE is an AI-driven content generation toolkit that helps creators instantly generate:
7
+
8
+ 🎬 Video Ideas
9
+
10
+ 📝 Scripts
11
+
12
+ 🗣 Subtitles
13
+
14
+ 🖼 Thumbnail Concepts & AI Prompts
15
+
16
+ Built for creators who want speed, consistency, and higher engagement without wasting hours brainstorming.
17
+
18
+ 🔥 Features
19
+ 1️⃣ Idea Generator
20
+
21
+ Generates high-potential content ideas based on:
22
+
23
+ Niche
24
+
25
+ Target Audience
26
+
27
+ Content Type (Shorts, Long-form, Reels, etc.)
28
+
29
+ Trending-style prompt patterns
30
+
31
+ Focus: Ideas engineered for attention and scalability, not generic topics.
32
+
33
+ 2️⃣ Script Generator
34
+
35
+ Creates structured, retention-optimized scripts including:
36
+
37
+ Hook
38
+
39
+ Pattern Interrupts
40
+
41
+ Retention Loops
42
+
43
+ Body Content
44
+
45
+ Call-to-Action (CTA)
46
+
47
+ Supports:
48
+
49
+ Short-form (30–60 sec)
50
+
51
+ Long-form YouTube scripts
52
+
53
+ Educational breakdowns
54
+
55
+ Storytelling formats
56
+
57
+ 3️⃣ Subtitle Generator
58
+
59
+ Generates subtitles designed for engagement:
60
+
61
+ Short, punchy lines
62
+
63
+ Scroll-stopping formatting
64
+
65
+ Retention-focused phrasing
66
+
67
+ Optional SRT-style structure support
68
+
69
+ 4️⃣ Thumbnail Generator
70
+
71
+ Generates:
72
+
73
+ High-CTR thumbnail text
74
+
75
+ Visual direction concepts
76
+
77
+ AI image prompts (Midjourney / DALL·E / Stable Diffusion ready)
78
+
79
+ Built around:
80
+
81
+ Curiosity Gap
82
+
83
+ Emotional Triggers
84
+
85
+ Click-through psychology
86
+
87
+ 🧠 How It Works
88
+
89
+ CLPFORGE uses prompt engineering + AI models to:
90
+
91
+ Analyze niche or topic input
92
+
93
+ Generate multiple idea variations
94
+
95
+ Structure scripts for audience retention
96
+
97
+ Format subtitles for engagement
98
+
99
+ Suggest high-CTR thumbnail concepts
100
+
101
+ The system focuses on content performance, not just content generation.
102
+
103
+ 🎯 Target Users
104
+
105
+ YouTube Creators
106
+
107
+ Short-form Content Creators
108
+
109
+ Digital Marketers
110
+
111
+ Content Agencies
112
+
113
+ Personal Brands
114
+
115
+ 🚧 Roadmap
116
+
117
+ Planned improvements:
118
+
119
+ Direct AI thumbnail image generation
120
+
121
+ Trend scraping integration
122
+
123
+ Multi-language support
124
+
125
+ SEO optimization module
126
+
127
+ Performance analytics dashboard
app.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import shutil
3
+ import os
4
+
5
+ from dotenv import load_dotenv
6
+ load_dotenv()
7
+
8
+ from modules.summarizer import summarize_text
9
+ from modules.frame_extractor import extract_frame
10
+ from modules.thumbnail import generate_thumbnail
11
+ from modules.composer import compose_videos
12
+ from modules.subtitles import generate_subtitles
13
+ from modules.subtitle_burner import burn_subtitles
14
+
15
+
16
+ UPLOAD_DIR = "assets/uploads"
17
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
18
+
19
+
20
+ # ---------------- FUNCTIONS ----------------
21
+
22
+ def save_uploaded_video(video_file):
23
+ if video_file is None:
24
+ return "❌ No video uploaded."
25
+
26
+ filename = os.path.basename(video_file)
27
+ destination = os.path.join(UPLOAD_DIR, filename)
28
+ shutil.copy(video_file, destination)
29
+
30
+ return f"✅ Video uploaded: {filename}"
31
+
32
+
33
+ def summarize_ui(text):
34
+ if not text.strip():
35
+ return "Please enter text."
36
+ return summarize_text(text)
37
+
38
+
39
+ def create_thumbnail(video_file, title_text):
40
+ if video_file is None:
41
+ return None
42
+ frame = extract_frame(video_file)
43
+ return generate_thumbnail(frame, title_text)
44
+
45
+
46
+ def compose_ui(video_files):
47
+ if not video_files:
48
+ return None
49
+ return compose_videos(video_files)
50
+
51
+
52
+ def subtitle_ui(video):
53
+ if video is None:
54
+ return None
55
+ return generate_subtitles(video)
56
+
57
+
58
+ def burn_subtitles_ui(video, subtitle_file):
59
+ if video is None or subtitle_file is None:
60
+ return None
61
+ return burn_subtitles(video, subtitle_file)
62
+
63
+
64
+ # ---------------- CSS ----------------
65
+
66
+ custom_css = """
67
+ body{
68
+ background:linear-gradient(135deg,#020617,#0f172a);
69
+ font-family:Inter;
70
+ }
71
+
72
+ .hero{
73
+ text-align:center;
74
+ padding:50px;
75
+ }
76
+
77
+ .hero-title{
78
+ font-size:44px;
79
+ font-weight:800;
80
+ background:linear-gradient(90deg,#60a5fa,#a78bfa,#c084fc);
81
+ -webkit-background-clip:text;
82
+ -webkit-text-fill-color:transparent;
83
+ }
84
+
85
+ .navbar{
86
+ display:flex;
87
+ gap:20px;
88
+ justify-content:center;
89
+ margin-bottom:25px;
90
+ }
91
+
92
+ .card{
93
+ background:#0f172a;
94
+ padding:25px;
95
+ border-radius:14px;
96
+ box-shadow:0 8px 20px rgba(0,0,0,0.4);
97
+ }
98
+ """
99
+
100
+
101
+ # ---------------- UI ----------------
102
+
103
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as app:
104
+
105
+ with gr.Column(elem_classes="hero"):
106
+ gr.Markdown("""
107
+ <div class="hero-title">🎬 ClipForge AI</div>
108
+ Create viral short-form content using AI
109
+ """)
110
+
111
+ # NAVIGATION BAR
112
+ with gr.Row():
113
+ btn_upload = gr.Button("📥 Upload")
114
+ btn_summary = gr.Button("📝 Summary")
115
+ btn_thumbnail = gr.Button("🖼 Thumbnail")
116
+ btn_subtitle = gr.Button("🎤 Subtitles")
117
+ btn_composer = gr.Button("🎞 Composer")
118
+ btn_burn = gr.Button("🔥 Burn")
119
+
120
+ # SECTIONS
121
+
122
+ with gr.Column(visible=True) as upload_section:
123
+ with gr.Column(elem_classes="card"):
124
+ video_input = gr.Video()
125
+ upload_btn = gr.Button("Upload Video")
126
+ upload_status = gr.Textbox(label="Status")
127
+
128
+ upload_btn.click(
129
+ save_uploaded_video,
130
+ inputs=video_input,
131
+ outputs=upload_status
132
+ )
133
+
134
+
135
+ with gr.Column(visible=False) as summary_section:
136
+ with gr.Column(elem_classes="card"):
137
+ input_text = gr.Textbox(lines=5)
138
+ summarize_btn = gr.Button("Generate Summary")
139
+ output_text = gr.Textbox()
140
+
141
+ summarize_btn.click(
142
+ summarize_ui,
143
+ inputs=input_text,
144
+ outputs=output_text
145
+ )
146
+
147
+
148
+ with gr.Column(visible=False) as thumb_section:
149
+ with gr.Column(elem_classes="card"):
150
+ video_for_thumb = gr.Video()
151
+ title_text = gr.Textbox()
152
+ thumb_btn = gr.Button("Generate Thumbnail")
153
+ thumbnail_output = gr.Image()
154
+
155
+ thumb_btn.click(
156
+ create_thumbnail,
157
+ inputs=[video_for_thumb, title_text],
158
+ outputs=thumbnail_output
159
+ )
160
+
161
+
162
+ with gr.Column(visible=False) as subtitle_section:
163
+ with gr.Column(elem_classes="card"):
164
+ video_input_sub = gr.Video()
165
+ subtitle_btn = gr.Button("Generate Subtitles")
166
+ subtitle_file = gr.File()
167
+
168
+ subtitle_btn.click(
169
+ subtitle_ui,
170
+ inputs=video_input_sub,
171
+ outputs=subtitle_file
172
+ )
173
+
174
+
175
+ with gr.Column(visible=False) as composer_section:
176
+ with gr.Column(elem_classes="card"):
177
+ video_inputs = gr.File(file_types=[".mp4"], file_count="multiple")
178
+ compose_btn = gr.Button("Compose Video")
179
+ final_video = gr.Video()
180
+
181
+ compose_btn.click(
182
+ compose_ui,
183
+ inputs=video_inputs,
184
+ outputs=final_video
185
+ )
186
+
187
+
188
+ with gr.Column(visible=False) as burn_section:
189
+ with gr.Column(elem_classes="card"):
190
+ video_input_burn = gr.Video()
191
+ subtitle_input = gr.File()
192
+ burn_btn = gr.Button("Burn Subtitles")
193
+ output_video = gr.Video()
194
+
195
+ burn_btn.click(
196
+ burn_subtitles_ui,
197
+ inputs=[video_input_burn, subtitle_input],
198
+ outputs=output_video
199
+ )
200
+
201
+
202
+ # ---------- NAVIGATION FUNCTIONS (FIXED) ----------
203
+
204
+ def show_upload():
205
+ return (
206
+ gr.update(visible=True),
207
+ gr.update(visible=False),
208
+ gr.update(visible=False),
209
+ gr.update(visible=False),
210
+ gr.update(visible=False),
211
+ gr.update(visible=False),
212
+ )
213
+
214
+
215
+ def show_summary():
216
+ return (
217
+ gr.update(visible=False),
218
+ gr.update(visible=True),
219
+ gr.update(visible=False),
220
+ gr.update(visible=False),
221
+ gr.update(visible=False),
222
+ gr.update(visible=False),
223
+ )
224
+
225
+
226
+ def show_thumb():
227
+ return (
228
+ gr.update(visible=False),
229
+ gr.update(visible=False),
230
+ gr.update(visible=True),
231
+ gr.update(visible=False),
232
+ gr.update(visible=False),
233
+ gr.update(visible=False),
234
+ )
235
+
236
+
237
+ def show_subtitle():
238
+ return (
239
+ gr.update(visible=False),
240
+ gr.update(visible=False),
241
+ gr.update(visible=False),
242
+ gr.update(visible=True),
243
+ gr.update(visible=False),
244
+ gr.update(visible=False),
245
+ )
246
+
247
+
248
+ def show_composer():
249
+ return (
250
+ gr.update(visible=False),
251
+ gr.update(visible=False),
252
+ gr.update(visible=False),
253
+ gr.update(visible=False),
254
+ gr.update(visible=True),
255
+ gr.update(visible=False),
256
+ )
257
+
258
+
259
+ def show_burn():
260
+ return (
261
+ gr.update(visible=False),
262
+ gr.update(visible=False),
263
+ gr.update(visible=False),
264
+ gr.update(visible=False),
265
+ gr.update(visible=False),
266
+ gr.update(visible=True),
267
+ )
268
+
269
+
270
+ btn_upload.click(show_upload,
271
+ outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
272
+
273
+ btn_summary.click(show_summary,
274
+ outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
275
+
276
+ btn_thumbnail.click(show_thumb,
277
+ outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
278
+
279
+ btn_subtitle.click(show_subtitle,
280
+ outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
281
+
282
+ btn_composer.click(show_composer,
283
+ outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
284
+
285
+ btn_burn.click(show_burn,
286
+ outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
287
+
288
+
289
+ app.launch()
modules/composer.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from moviepy import VideoFileClip, concatenate_videoclips
2
+ import os
3
+ import shutil
4
+
5
+ OUTPUT_DIR = "assets/outputs"
6
+ UPLOAD_DIR = "assets/uploads"
7
+
8
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
9
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
10
+
11
+ def normalize_video(input_path, output_path):
12
+ """
13
+ Re-encode video to constant frame rate and valid keyframe.
14
+ This fixes WhatsApp/mobile video issues on Windows.
15
+ """
16
+ clip = VideoFileClip(input_path)
17
+ clip.write_videofile(
18
+ output_path,
19
+ codec="libx264",
20
+ audio_codec="aac",
21
+ fps=24,
22
+ preset="medium",
23
+ ffmpeg_params=["-movflags", "+faststart"]
24
+ )
25
+ clip.close()
26
+
27
+ def compose_videos(video_files):
28
+ clips = []
29
+ normalized_paths = []
30
+
31
+ for file in video_files:
32
+ filename = os.path.basename(file)
33
+ stable_path = os.path.join(UPLOAD_DIR, filename)
34
+ shutil.copy(file, stable_path)
35
+
36
+ normalized_path = os.path.join(
37
+ UPLOAD_DIR, f"normalized_{filename}"
38
+ )
39
+
40
+ normalize_video(stable_path, normalized_path)
41
+ normalized_paths.append(normalized_path)
42
+
43
+ for path in normalized_paths:
44
+ clips.append(VideoFileClip(path))
45
+
46
+ final_clip = concatenate_videoclips(clips)
47
+ output_path = os.path.join(OUTPUT_DIR, "final_video.mp4")
48
+
49
+ final_clip.write_videofile(
50
+ output_path,
51
+ codec="libx264",
52
+ audio_codec="aac"
53
+ )
54
+
55
+ for clip in clips:
56
+ clip.close()
57
+
58
+ return output_path
modules/frame_extractor.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import os
3
+
4
+ OUTPUT_DIR = "assets/outputs"
5
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
6
+
7
+ def extract_frame(video_path):
8
+ cap = cv2.VideoCapture(video_path)
9
+
10
+ success, frame = cap.read()
11
+ cap.release()
12
+
13
+ if not success:
14
+ raise ValueError("Could not read frame from video.")
15
+
16
+ frame_path = os.path.join(OUTPUT_DIR, "frame.jpg")
17
+ cv2.imwrite(frame_path, frame)
18
+
19
+ return frame_path
modules/subtitle_burner.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from moviepy import VideoFileClip, TextClip, CompositeVideoClip
2
+ import pysrt
3
+ import os
4
+
5
+ OUTPUT_DIR = "assets/outputs"
6
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
7
+
8
+ def burn_subtitles(video_path, subtitle_path):
9
+
10
+ video = VideoFileClip(video_path)
11
+ subs = pysrt.open(subtitle_path)
12
+
13
+ subtitle_clips = []
14
+
15
+ for sub in subs:
16
+
17
+ start = sub.start.ordinal / 1000
18
+ end = sub.end.ordinal / 1000
19
+
20
+ txt_clip = TextClip(
21
+ text=sub.text,
22
+ font_size=40,
23
+ color="white",
24
+ stroke_color="black",
25
+ stroke_width=2,
26
+ size=(int(video.w * 0.9), None),
27
+ method="caption"
28
+ )
29
+
30
+ txt_clip = txt_clip.with_start(start).with_end(end).with_position(("center","bottom"))
31
+
32
+ subtitle_clips.append(txt_clip)
33
+
34
+ final = CompositeVideoClip([video] + subtitle_clips)
35
+
36
+ output_path = os.path.join(OUTPUT_DIR, "captioned_video.mp4")
37
+
38
+ final.write_videofile(output_path, codec="libx264")
39
+
40
+ return output_path
modules/subtitles.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from faster_whisper import WhisperModel
2
+ import os
3
+
4
+ OUTPUT_DIR = "assets/outputs"
5
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
6
+
7
+ # Force CPU usage
8
+ model = WhisperModel("base", device="cpu", compute_type="int8")
9
+
10
+ def generate_subtitles(video_path):
11
+
12
+ segments, info = model.transcribe(video_path)
13
+
14
+ srt_path = os.path.join(OUTPUT_DIR, "subtitles.srt")
15
+
16
+ with open(srt_path, "w", encoding="utf-8") as srt:
17
+
18
+ for i, segment in enumerate(segments, start=1):
19
+
20
+ start = format_time(segment.start)
21
+ end = format_time(segment.end)
22
+ text = segment.text.strip()
23
+
24
+ srt.write(f"{i}\n")
25
+ srt.write(f"{start} --> {end}\n")
26
+ srt.write(f"{text}\n\n")
27
+
28
+ return srt_path
29
+
30
+
31
+ def format_time(seconds):
32
+
33
+ hrs = int(seconds // 3600)
34
+ mins = int((seconds % 3600) // 60)
35
+ secs = int(seconds % 60)
36
+ millis = int((seconds - int(seconds)) * 1000)
37
+
38
+ return f"{hrs:02}:{mins:02}:{secs:02},{millis:03}"
modules/summarizer.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import InferenceClient
2
+ import os
3
+
4
+ client = InferenceClient(token=os.getenv("HF_TOKEN"))
5
+
6
+
7
+ def summarize_text(text):
8
+ response = client.summarization(
9
+ text,
10
+ model="facebook/bart-large-cnn"
11
+ )
12
+ return response["summary_text"]
modules/thumbnail.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image, ImageDraw, ImageFont
2
+ import os
3
+
4
+ OUTPUT_DIR = "assets/outputs"
5
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
6
+
7
+ def generate_thumbnail(image_path, text):
8
+ if not os.path.exists(image_path):
9
+ raise FileNotFoundError("Frame image not found.")
10
+
11
+ # 1. Load & resize to YouTube thumbnail size
12
+ img = Image.open(image_path).convert("RGB")
13
+ img = img.resize((1280, 720))
14
+
15
+ draw = ImageDraw.Draw(img)
16
+
17
+ # 2. Add dark gradient overlay (bottom)
18
+ overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
19
+ overlay_draw = ImageDraw.Draw(overlay)
20
+
21
+ for y in range(400, 720):
22
+ alpha = int((y - 400) / 320 * 180)
23
+ overlay_draw.rectangle(
24
+ [(0, y), (1280, y + 1)],
25
+ fill=(0, 0, 0, alpha)
26
+ )
27
+
28
+ img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
29
+ draw = ImageDraw.Draw(img)
30
+
31
+ # 3. Load font (fallback safe)
32
+ try:
33
+ font = ImageFont.truetype("arialbd.ttf", 80)
34
+ except:
35
+ font = ImageFont.load_default()
36
+
37
+ # 4. Draw bold title text (bottom-left)
38
+ x, y = 60, 500
39
+
40
+ # Text shadow
41
+ draw.text((x + 4, y + 4), text, font=font, fill="black")
42
+ draw.text((x, y), text, font=font, fill="white")
43
+
44
+ # 5. Save thumbnail
45
+ output_path = os.path.join(OUTPUT_DIR, "thumbnail.jpg")
46
+ img.save(output_path, quality=95)
47
+
48
+ return output_path
requirements.txt ADDED
Binary file (2.62 kB). View file