ginipick commited on
Commit
19aa20c
ยท
verified ยท
1 Parent(s): 2f797f3

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +330 -0
index.html ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Video, Upload, Loader2, Download, Settings } from 'lucide-react';
3
+
4
+ const VideoGenerator = () => {
5
+ const [mode, setMode] = useState('text-to-video');
6
+ const [prompt, setPrompt] = useState('');
7
+ const [imageFile, setImageFile] = useState(null);
8
+ const [imagePreview, setImagePreview] = useState('');
9
+ const [aspectRatio, setAspectRatio] = useState('16:9');
10
+ const [seed, setSeed] = useState(42);
11
+ const [isGenerating, setIsGenerating] = useState(false);
12
+ const [videoUrl, setVideoUrl] = useState('');
13
+ const [error, setError] = useState('');
14
+ const [apiKey, setApiKey] = useState('');
15
+ const [showApiKeyInput, setShowApiKeyInput] = useState(false);
16
+
17
+ const aspectRatioOptions = [
18
+ { value: '16:9', label: '16:9', description: '(YouTube, ์ผ๋ฐ˜ ๋™์˜์ƒ)' },
19
+ { value: '4:3', label: '4:3', description: '(์ „ํ†ต์ ์ธ TV ํ˜•์‹)' },
20
+ { value: '1:1', label: '1:1', description: '(Instagram ํ”ผ๋“œ)' },
21
+ { value: '3:4', label: '3:4', description: '(Instagram ํฌํŠธ๋ ˆ์ดํŠธ)' },
22
+ { value: '9:16', label: '9:16', description: '(Instagram ๋ฆด์Šค, TikTok)' },
23
+ { value: '21:9', label: '21:9', description: '(์‹œ๋„ค๋งˆํ‹ฑ ์™€์ด๋“œ)' },
24
+ { value: '9:21', label: '9:21', description: '(์šธํŠธ๋ผ ์„ธ๋กœํ˜•)' }
25
+ ];
26
+
27
+ const handleImageUpload = (e) => {
28
+ const file = e.target.files[0];
29
+ if (file) {
30
+ setImageFile(file);
31
+ const reader = new FileReader();
32
+ reader.onloadend = () => {
33
+ setImagePreview(reader.result);
34
+ };
35
+ reader.readAsDataURL(file);
36
+ }
37
+ };
38
+
39
+ const generateVideo = async () => {
40
+ if (!prompt.trim()) {
41
+ setError('ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
42
+ return;
43
+ }
44
+
45
+ if (mode === 'image-to-video' && !imageFile) {
46
+ setError('์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.');
47
+ return;
48
+ }
49
+
50
+ setIsGenerating(true);
51
+ setError('');
52
+ setVideoUrl('');
53
+
54
+ try {
55
+ // API ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ
56
+ const token = apiKey || process.env.RAPI_TOKEN;
57
+
58
+ if (!token) {
59
+ throw new Error('Replicate API ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„ค์ •์—์„œ API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
60
+ }
61
+
62
+ // Python ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค
63
+ const pythonCode = `
64
+ import os
65
+ import replicate
66
+
67
+ # API ํ† ํฐ ์„ค์ •
68
+ os.environ["REPLICATE_API_TOKEN"] = "${token}"
69
+
70
+ input = {
71
+ "prompt": "${prompt}",
72
+ "duration": 5,
73
+ "resolution": "480p",
74
+ "aspect_ratio": "${aspectRatio}",
75
+ "seed": ${seed}${mode === 'image-to-video' ? ',\n "image": "' + imagePreview + '"' : ''}
76
+ }
77
+
78
+ output = replicate.run(
79
+ "bytedance/seedance-1-lite",
80
+ input=input
81
+ )
82
+
83
+ # ๋น„๋””์˜ค URL ๋ฐ˜ํ™˜
84
+ print(output)
85
+ `;
86
+
87
+ // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๋ฐฑ์—”๋“œ API๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
88
+ console.log('์ƒ์„ฑํ•  Python ์ฝ”๋“œ:', pythonCode);
89
+
90
+ // ๋ฐ๋ชจ๋ฅผ ์œ„ํ•œ ๊ฐ€์งœ ์‘๋‹ต
91
+ setTimeout(() => {
92
+ setVideoUrl('https://example.com/generated-video.mp4');
93
+ setIsGenerating(false);
94
+ }, 3000);
95
+
96
+ } catch (err) {
97
+ setError(err.message);
98
+ setIsGenerating(false);
99
+ }
100
+ };
101
+
102
+ return (
103
+ <div className="min-h-screen bg-gray-900 text-white p-8">
104
+ <div className="max-w-4xl mx-auto">
105
+ <h1 className="text-4xl font-bold mb-8 text-center">AI Video Generator</h1>
106
+
107
+ {/* API ํ‚ค ์„ค์ • */}
108
+ <div className="mb-6">
109
+ <button
110
+ onClick={() => setShowApiKeyInput(!showApiKeyInput)}
111
+ className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
112
+ >
113
+ <Settings size={20} />
114
+ API ์„ค์ •
115
+ </button>
116
+
117
+ {showApiKeyInput && (
118
+ <div className="mt-2">
119
+ <input
120
+ type="password"
121
+ placeholder="Replicate API ํ† ํฐ ์ž…๋ ฅ (์„ ํƒ์‚ฌํ•ญ)"
122
+ value={apiKey}
123
+ onChange={(e) => setApiKey(e.target.value)}
124
+ className="w-full p-2 bg-gray-800 rounded border border-gray-700 focus:border-blue-500 outline-none"
125
+ />
126
+ <p className="text-xs text-gray-500 mt-1">
127
+ ํ™˜๊ฒฝ๋ณ€์ˆ˜ RAPI_TOKEN์ด ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ ์—ฌ๊ธฐ์— ์ž…๋ ฅํ•˜์„ธ์š”.
128
+ </p>
129
+ </div>
130
+ )}
131
+ </div>
132
+
133
+ {/* ๋ชจ๋“œ ์„ ํƒ */}
134
+ <div className="mb-8">
135
+ <h2 className="text-xl font-semibold mb-4">์ƒ์„ฑ ๋ชจ๋“œ ์„ ํƒ</h2>
136
+ <div className="grid grid-cols-2 gap-4">
137
+ <button
138
+ onClick={() => setMode('text-to-video')}
139
+ className={`p-4 rounded-lg border-2 transition-all ${
140
+ mode === 'text-to-video'
141
+ ? 'border-blue-500 bg-blue-500/20'
142
+ : 'border-gray-700 hover:border-gray-600'
143
+ }`}
144
+ >
145
+ <Video className="mb-2" size={32} />
146
+ <h3 className="font-semibold">ํ…์Šค๏ฟฝ๏ฟฝ to ๋น„๋””์˜ค</h3>
147
+ <p className="text-sm text-gray-400">ํ…์ŠคํŠธ ์„ค๋ช…์œผ๋กœ ๋น„๋””์˜ค ์ƒ์„ฑ</p>
148
+ </button>
149
+
150
+ <button
151
+ onClick={() => setMode('image-to-video')}
152
+ className={`p-4 rounded-lg border-2 transition-all ${
153
+ mode === 'image-to-video'
154
+ ? 'border-blue-500 bg-blue-500/20'
155
+ : 'border-gray-700 hover:border-gray-600'
156
+ }`}
157
+ >
158
+ <Upload className="mb-2" size={32} />
159
+ <h3 className="font-semibold">์ด๋ฏธ์ง€ to ๋น„๋””์˜ค</h3>
160
+ <p className="text-sm text-gray-400">์ด๋ฏธ์ง€๋ฅผ ์›€์ง์ด๋Š” ๋น„๋””์˜ค๋กœ ๋ณ€ํ™˜</p>
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ {/* ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ (์ด๋ฏธ์ง€ to ๋น„๋””์˜ค ๋ชจ๋“œ) */}
166
+ {mode === 'image-to-video' && (
167
+ <div className="mb-6">
168
+ <h2 className="text-xl font-semibold mb-4">์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</h2>
169
+ <div className="border-2 border-dashed border-gray-700 rounded-lg p-8 text-center">
170
+ {imagePreview ? (
171
+ <div className="space-y-4">
172
+ <img src={imagePreview} alt="Preview" className="max-h-64 mx-auto rounded" />
173
+ <button
174
+ onClick={() => {
175
+ setImageFile(null);
176
+ setImagePreview('');
177
+ }}
178
+ className="text-red-400 hover:text-red-300"
179
+ >
180
+ ์ด๋ฏธ์ง€ ์ œ๊ฑฐ
181
+ </button>
182
+ </div>
183
+ ) : (
184
+ <>
185
+ <Upload size={48} className="mx-auto mb-4 text-gray-600" />
186
+ <input
187
+ type="file"
188
+ accept="image/*"
189
+ onChange={handleImageUpload}
190
+ className="hidden"
191
+ id="image-upload"
192
+ />
193
+ <label
194
+ htmlFor="image-upload"
195
+ className="cursor-pointer text-blue-400 hover:text-blue-300"
196
+ >
197
+ ์ด๋ฏธ์ง€ ์„ ํƒ
198
+ </label>
199
+ </>
200
+ )}
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {/* ํ”„๋กฌํ”„ํŠธ ์ž…๋ ฅ */}
206
+ <div className="mb-6">
207
+ <h2 className="text-xl font-semibold mb-4">
208
+ {mode === 'text-to-video' ? '๋น„๋””์˜ค ์„ค๋ช…' : '์˜์ƒ ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ'}
209
+ </h2>
210
+ <textarea
211
+ value={prompt}
212
+ onChange={(e) => setPrompt(e.target.value)}
213
+ placeholder={
214
+ mode === 'text-to-video'
215
+ ? "์ƒ์„ฑํ•  ๋น„๋””์˜ค๋ฅผ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. ์˜ˆ: The sun rises slowly between tall buildings..."
216
+ : "์ด๋ฏธ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ์›€์ง์ด๊ฒŒ ํ• ์ง€ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. ์˜ˆ: Camera slowly zooms in while clouds move..."
217
+ }
218
+ className="w-full h-32 p-4 bg-gray-800 rounded-lg border border-gray-700 focus:border-blue-500 outline-none resize-none"
219
+ />
220
+ </div>
221
+
222
+ {/* ์„ค์ • ์˜ต์…˜ */}
223
+ <div className="mb-8 space-y-4">
224
+ <h2 className="text-xl font-semibold mb-4">์„ค์ •</h2>
225
+
226
+ {/* ํ™”๋ฉด ๋น„์œจ ์„ ํƒ */}
227
+ <div>
228
+ <label className="block text-sm font-medium mb-2">ํ™”๋ฉด ๋น„์œจ</label>
229
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
230
+ {aspectRatioOptions.map((option) => (
231
+ <button
232
+ key={option.value}
233
+ onClick={() => setAspectRatio(option.value)}
234
+ className={`p-3 rounded border transition-all ${
235
+ aspectRatio === option.value
236
+ ? 'border-blue-500 bg-blue-500/20'
237
+ : 'border-gray-700 hover:border-gray-600'
238
+ }`}
239
+ >
240
+ <div className="font-semibold">{option.label}</div>
241
+ <div className="text-xs text-gray-400">{option.description}</div>
242
+ </button>
243
+ ))}
244
+ </div>
245
+ </div>
246
+
247
+ {/* Seed ์„ค์ • */}
248
+ <div>
249
+ <label className="block text-sm font-medium mb-2">Seed (๋žœ๋ค ์‹œ๋“œ)</label>
250
+ <input
251
+ type="number"
252
+ value={seed}
253
+ onChange={(e) => setSeed(parseInt(e.target.value) || 0)}
254
+ className="w-full p-2 bg-gray-800 rounded border border-gray-700 focus:border-blue-500 outline-none"
255
+ />
256
+ <p className="text-xs text-gray-500 mt-1">๋™์ผํ•œ ์‹œ๋“œ๊ฐ’์œผ๋กœ ๋™์ผํ•œ ๊ฒฐ๊ณผ๋ฅผ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
257
+ </div>
258
+
259
+ {/* ๊ณ ์ • ์„ค์ • ํ‘œ์‹œ */}
260
+ <div className="text-sm text-gray-400">
261
+ <p>โ€ข ํ•ด์ƒ๋„: 480p (๊ณ ์ •)</p>
262
+ <p>โ€ข ์žฌ์ƒ ์‹œ๊ฐ„: 5์ดˆ (๊ณ ์ •)</p>
263
+ </div>
264
+ </div>
265
+
266
+ {/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */}
267
+ {error && (
268
+ <div className="mb-4 p-4 bg-red-500/20 border border-red-500 rounded-lg text-red-400">
269
+ {error}
270
+ </div>
271
+ )}
272
+
273
+ {/* ์ƒ์„ฑ ๋ฒ„ํŠผ */}
274
+ <button
275
+ onClick={generateVideo}
276
+ disabled={isGenerating}
277
+ className="w-full py-4 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2"
278
+ >
279
+ {isGenerating ? (
280
+ <>
281
+ <Loader2 className="animate-spin" size={20} />
282
+ ๋น„๋””์˜ค ์ƒ์„ฑ ์ค‘...
283
+ </>
284
+ ) : (
285
+ <>
286
+ <Video size={20} />
287
+ ๋น„๋””์˜ค ์ƒ์„ฑ
288
+ </>
289
+ )}
290
+ </button>
291
+
292
+ {/* ๊ฒฐ๊ณผ ํ‘œ์‹œ */}
293
+ {videoUrl && (
294
+ <div className="mt-8 p-6 bg-gray-800 rounded-lg">
295
+ <h3 className="text-xl font-semibold mb-4">์ƒ์„ฑ๋œ ๋น„๋””์˜ค</h3>
296
+ <video
297
+ src={videoUrl}
298
+ controls
299
+ className="w-full rounded mb-4"
300
+ />
301
+ <a
302
+ href={videoUrl}
303
+ download="generated-video.mp4"
304
+ className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 rounded transition-colors"
305
+ >
306
+ <Download size={20} />
307
+ ๋น„๋””์˜ค ๋‹ค์šด๋กœ๋“œ
308
+ </a>
309
+ </div>
310
+ )}
311
+
312
+ {/* ๊ตฌํ˜„ ๋…ธํŠธ */}
313
+ <div className="mt-12 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
314
+ <h4 className="font-semibold text-yellow-400 mb-2">๊ตฌํ˜„ ๋…ธํŠธ</h4>
315
+ <p className="text-sm text-gray-300">
316
+ ์ด ์ธํ„ฐํŽ˜์ด์Šค๋Š” ๋ฐ๋ชจ์šฉ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ์ž‘๋™ํ•˜๋ ค๋ฉด:
317
+ </p>
318
+ <ul className="text-sm text-gray-300 mt-2 list-disc list-inside space-y-1">
319
+ <li>๋ฐฑ์—”๋“œ ์„œ๋ฒ„์—์„œ Replicate API๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค</li>
320
+ <li>ํ™˜๊ฒฝ๋ณ€์ˆ˜ RAPI_TOKEN์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค</li>
321
+ <li>bytedance/seedance-1-lite ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค</li>
322
+ <li>์ƒ์„ฑ๋œ ๋น„๋””์˜ค๋Š” ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌ ํ›„ ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค</li>
323
+ </ul>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ );
328
+ };
329
+
330
+ export default VideoGenerator;