Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Simple Video Editor - Canvas ๊ธฐ๋ฐ ๋ ๋๋ง
|
| 3 |
-
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import gradio as gr
|
|
@@ -12,8 +13,9 @@ import tempfile
|
|
| 12 |
import shutil
|
| 13 |
import time
|
| 14 |
|
|
|
|
| 15 |
UPLOAD_DIR = tempfile.mkdtemp()
|
| 16 |
-
uploaded_files = {}
|
| 17 |
|
| 18 |
def get_editor_html(media_data="[]"):
|
| 19 |
return f'''<!DOCTYPE html>
|
|
@@ -269,6 +271,7 @@ drawPlaceholder();
|
|
| 269 |
setupCanvasDrag();
|
| 270 |
}}
|
| 271 |
|
|
|
|
| 272 |
function setupCanvasDrag(){{
|
| 273 |
var canvas=S.canvas;
|
| 274 |
var dragging=null;
|
|
@@ -281,6 +284,7 @@ var scaleY=canvas.height/rect.height;
|
|
| 281 |
var mx=(e.clientX-rect.left)*scaleX;
|
| 282 |
var my=(e.clientY-rect.top)*scaleY;
|
| 283 |
|
|
|
|
| 284 |
var textClips=getTextClipsAt(S.time);
|
| 285 |
for(var i=textClips.length-1;i>=0;i--){{
|
| 286 |
var tc=textClips[i];
|
|
@@ -313,6 +317,7 @@ dragging.posX=Math.max(0.1,Math.min(0.9,(mx-dragOffsetX)/size.pw));
|
|
| 313 |
dragging.posY=Math.max(0.1,Math.min(0.9,(my-dragOffsetY)/size.ph));
|
| 314 |
drawFrame();
|
| 315 |
}}else{{
|
|
|
|
| 316 |
var textClips=getTextClipsAt(S.time);
|
| 317 |
var onText=false;
|
| 318 |
for(var i=0;i<textClips.length;i++){{
|
|
@@ -544,6 +549,7 @@ save();
|
|
| 544 |
var c=S.clips.find(function(x){{return x.id===cid}});
|
| 545 |
if(c){{
|
| 546 |
c.start=r(Math.max(0,t-ox/(S.pps*S.zoom)));
|
|
|
|
| 547 |
if(c.type==='text'){{
|
| 548 |
c.track=2;
|
| 549 |
}}else if(c.type==='audio'){{
|
|
@@ -601,6 +607,7 @@ if(!c){{box.innerHTML='<div class="no-sel">ํด๋ฆฝ ์ ํ</div>';return}}
|
|
| 601 |
var len=r(c.te-c.ts);
|
| 602 |
|
| 603 |
if(c.type==='text'){{
|
|
|
|
| 604 |
box.innerHTML='<div class="prop-group"><div class="prop-label">ํ
์คํธ</div><input class="prop-input" value="'+c.text+'" onchange="setProp(\\'text\\',this.value)"></div>'+
|
| 605 |
'<div class="prop-group"><div class="prop-label">์์</div><input class="prop-input" type="number" step="0.1" value="'+c.start+'" onchange="setProp(\\'start\\',parseFloat(this.value))"></div>'+
|
| 606 |
'<div class="prop-group"><div class="prop-label">๊ธธ์ด: '+fmt(len)+'</div></div>'+
|
|
@@ -621,6 +628,7 @@ box.innerHTML='<div class="prop-group"><div class="prop-label">ํ
์คํธ</div><i
|
|
| 621 |
'</select></div>'+
|
| 622 |
'<div class="prop-group"><div class="prop-label">๋ฐฐ๊ฒฝ ์์</div><input class="prop-input" type="color" value="'+c.bgColor+'" onchange="setProp(\\'bgColor\\',this.value)"></div>';
|
| 623 |
}}else{{
|
|
|
|
| 624 |
box.innerHTML='<div class="prop-group"><div class="prop-label">์ด๋ฆ</div><input class="prop-input" value="'+c.name+'" onchange="setProp(\\'name\\',this.value)"></div>'+
|
| 625 |
'<div class="prop-group"><div class="prop-label">์์</div><input class="prop-input" type="number" step="0.1" value="'+c.start+'" onchange="setProp(\\'start\\',parseFloat(this.value))"></div>'+
|
| 626 |
'<div class="prop-group"><div class="prop-label">๊ธธ์ด: '+fmt(len)+'</div></div>'+
|
|
@@ -776,7 +784,6 @@ S.ctx.font='14px sans-serif';
|
|
| 776 |
S.ctx.textAlign='center';
|
| 777 |
S.ctx.fillText('ํ์๋ผ์ธ์ ๋ฏธ๋์ด๋ฅผ ์ถ๊ฐํ์ธ์',pw/2,ph/2);
|
| 778 |
}}
|
| 779 |
-
|
| 780 |
var audioClips=S.clips.filter(function(c){{
|
| 781 |
if(c.type!=='audio')return false;
|
| 782 |
var cEnd=c.start+(c.te-c.ts);
|
|
@@ -810,6 +817,7 @@ S.ctx.fillText('์ฌ์ ์์น์ ๋ฏธ๋์ด๊ฐ ์์ต๋๋ค',pw/2,ph/2);
|
|
| 810 |
}}
|
| 811 |
}}
|
| 812 |
|
|
|
|
| 813 |
var textClips=getTextClipsAt(t);
|
| 814 |
textClips.forEach(function(tc){{
|
| 815 |
drawText(S.ctx,tc,pw,ph);
|
|
@@ -818,21 +826,25 @@ drawText(S.ctx,tc,pw,ph);
|
|
| 818 |
|
| 819 |
function drawWithMode(ctx,el,sw,sh,dw,dh,mode){{
|
| 820 |
if(mode==='fill'){{
|
|
|
|
| 821 |
var scale=Math.max(dw/sw,dh/sh);
|
| 822 |
var nw=sw*scale,nh=sh*scale;
|
| 823 |
var ox=(dw-nw)/2,oy=(dh-nh)/2;
|
| 824 |
ctx.drawImage(el,ox,oy,nw,nh);
|
| 825 |
}}else if(mode==='blur'){{
|
|
|
|
| 826 |
var frameCanvas=document.createElement('canvas');
|
| 827 |
frameCanvas.width=sw;frameCanvas.height=sh;
|
| 828 |
var frameCtx=frameCanvas.getContext('2d');
|
| 829 |
frameCtx.drawImage(el,0,0,sw,sh);
|
| 830 |
|
|
|
|
| 831 |
var blurCanvas=document.createElement('canvas');
|
| 832 |
blurCanvas.width=16;blurCanvas.height=16;
|
| 833 |
var blurCtx=blurCanvas.getContext('2d');
|
| 834 |
blurCtx.drawImage(frameCanvas,0,0,16,16);
|
| 835 |
|
|
|
|
| 836 |
ctx.imageSmoothingEnabled=true;
|
| 837 |
ctx.imageSmoothingQuality='high';
|
| 838 |
var scale=Math.max(dw/sw,dh/sh)*1.1;
|
|
@@ -840,14 +852,17 @@ var nw=sw*scale,nh=sh*scale;
|
|
| 840 |
var ox=(dw-nw)/2,oy=(dh-nh)/2;
|
| 841 |
ctx.drawImage(blurCanvas,ox,oy,nw,nh);
|
| 842 |
|
|
|
|
| 843 |
ctx.fillStyle='rgba(0,0,0,0.4)';
|
| 844 |
ctx.fillRect(0,0,dw,dh);
|
| 845 |
|
|
|
|
| 846 |
var fitScale=Math.min(dw/sw,dh/sh);
|
| 847 |
var fw=sw*fitScale,fh=sh*fitScale;
|
| 848 |
var fx=(dw-fw)/2,fy=(dh-fh)/2;
|
| 849 |
ctx.drawImage(frameCanvas,fx,fy,fw,fh);
|
| 850 |
}}else{{
|
|
|
|
| 851 |
var scale=Math.min(dw/sw,dh/sh);
|
| 852 |
var nw=sw*scale,nh=sh*scale;
|
| 853 |
var ox=(dw-nw)/2,oy=(dh-nh)/2;
|
|
@@ -857,6 +872,7 @@ ctx.drawImage(el,ox,oy,nw,nh);
|
|
| 857 |
|
| 858 |
function toggleMute(){{S.muted=!S.muted;document.getElementById('muteBtn').textContent=S.muted?'๐':'๐'}}
|
| 859 |
|
|
|
|
| 860 |
function addTextClip(){{
|
| 861 |
document.getElementById('textModal').style.display='flex';
|
| 862 |
document.getElementById('textInput').value='';
|
|
@@ -890,8 +906,8 @@ fontSize:parseInt(document.getElementById('textSize').value),
|
|
| 890 |
fontColor:document.getElementById('textColor').value,
|
| 891 |
bgStyle:document.getElementById('textBgStyle').value,
|
| 892 |
bgColor:document.getElementById('textBgColor').value,
|
| 893 |
-
posX:0.5,
|
| 894 |
-
posY:0.85
|
| 895 |
}});
|
| 896 |
closeTextModal();
|
| 897 |
renderTL();
|
|
@@ -927,6 +943,7 @@ var textH=fontSize;
|
|
| 927 |
var x=dw*posX;
|
| 928 |
var y=dh*posY;
|
| 929 |
|
|
|
|
| 930 |
clip._bounds={{
|
| 931 |
x:x-textW/2-fontSize*0.3,
|
| 932 |
y:y-textH/2-fontSize*0.15,
|
|
@@ -951,12 +968,15 @@ ctx.fillRect(bgX,bgY,bgW,bgH);
|
|
| 951 |
}}
|
| 952 |
}}
|
| 953 |
|
|
|
|
| 954 |
ctx.fillStyle='rgba(0,0,0,0.5)';
|
| 955 |
ctx.fillText(text,x+2,y+2);
|
| 956 |
|
|
|
|
| 957 |
ctx.fillStyle=fontColor;
|
| 958 |
ctx.fillText(text,x,y);
|
| 959 |
|
|
|
|
| 960 |
if(S.sel===clip.id){{
|
| 961 |
ctx.strokeStyle='#6366f1';
|
| 962 |
ctx.lineWidth=2;
|
|
@@ -965,7 +985,6 @@ ctx.strokeRect(clip._bounds.x,clip._bounds.y,clip._bounds.w,clip._bounds.h);
|
|
| 965 |
ctx.setLineDash([]);
|
| 966 |
}}
|
| 967 |
}}
|
| 968 |
-
|
| 969 |
function setZoom(v){{S.zoom=parseFloat(v);renderTL();updateHead()}}
|
| 970 |
function tlClick(e){{
|
| 971 |
if(e.target.closest('.clip'))return;
|
|
@@ -990,54 +1009,29 @@ doExport();
|
|
| 990 |
}}
|
| 991 |
|
| 992 |
// ============================================
|
| 993 |
-
// ํต์ฌ ์์ :
|
| 994 |
// ============================================
|
| 995 |
-
|
| 996 |
-
function sleep(ms){{
|
| 997 |
-
return new Promise(function(resolve){{setTimeout(resolve,ms)}});
|
| 998 |
-
}}
|
| 999 |
-
|
| 1000 |
-
function seekVideoAndWait(video,targetTime){{
|
| 1001 |
-
return new Promise(function(resolve){{
|
| 1002 |
-
if(Math.abs(video.currentTime-targetTime)<0.02){{
|
| 1003 |
-
resolve();
|
| 1004 |
-
return;
|
| 1005 |
-
}}
|
| 1006 |
-
var resolved=false;
|
| 1007 |
-
function onSeeked(){{
|
| 1008 |
-
if(resolved)return;
|
| 1009 |
-
resolved=true;
|
| 1010 |
-
video.removeEventListener('seeked',onSeeked);
|
| 1011 |
-
resolve();
|
| 1012 |
-
}}
|
| 1013 |
-
video.addEventListener('seeked',onSeeked);
|
| 1014 |
-
video.currentTime=targetTime;
|
| 1015 |
-
// ํ์์์ (์ํน์ด ์ค๋ ๊ฑธ๋ฆด ๊ฒฝ์ฐ ๋๋น)
|
| 1016 |
-
setTimeout(function(){{
|
| 1017 |
-
if(!resolved){{
|
| 1018 |
-
resolved=true;
|
| 1019 |
-
video.removeEventListener('seeked',onSeeked);
|
| 1020 |
-
resolve();
|
| 1021 |
-
}}
|
| 1022 |
-
}},300);
|
| 1023 |
-
}});
|
| 1024 |
-
}}
|
| 1025 |
-
|
| 1026 |
async function doExport(){{
|
| 1027 |
var size=S.ratioSizes[S.ratio];
|
| 1028 |
var exportW=size.w,exportH=size.h;
|
| 1029 |
|
| 1030 |
-
// ๋ฉ์ธ ์บ๋ฒ์ค (MediaRecorder
|
| 1031 |
-
var
|
| 1032 |
-
|
| 1033 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
|
| 1035 |
// ์ด๊ธฐ ๊ฒ์ ํ๋ฉด
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
|
| 1039 |
-
var stream=
|
| 1040 |
|
|
|
|
| 1041 |
var opts={{mimeType:'video/webm;codecs=vp9',videoBitsPerSecond:8000000}};
|
| 1042 |
if(!MediaRecorder.isTypeSupported(opts.mimeType)){{
|
| 1043 |
opts={{mimeType:'video/webm;codecs=vp8',videoBitsPerSecond:8000000}};
|
|
@@ -1054,16 +1048,8 @@ document.getElementById('exportMsg').textContent='๋
นํ ์ค... ('+S.ratio+')';
|
|
| 1054 |
rec.start(100);
|
| 1055 |
|
| 1056 |
var dur=S.dur;
|
| 1057 |
-
var fps=30;
|
| 1058 |
-
var totalFrames=Math.ceil(dur*fps);
|
| 1059 |
var fillMode=S.fillMode;
|
| 1060 |
|
| 1061 |
-
// ๋ชจ๋ ๋น๋์ค ์ผ์์ ์ง
|
| 1062 |
-
Object.keys(S.els).forEach(function(k){{
|
| 1063 |
-
var el=S.els[k];
|
| 1064 |
-
if(el&&el.pause)el.pause();
|
| 1065 |
-
}});
|
| 1066 |
-
|
| 1067 |
// ๋ธ๋ฌ์ฉ ์บ๋ฒ์ค (์ฌ์ฌ์ฉ)
|
| 1068 |
var frameCanvas=document.createElement('canvas');
|
| 1069 |
var frameCtx=frameCanvas.getContext('2d');
|
|
@@ -1071,30 +1057,56 @@ var blurCanvas=document.createElement('canvas');
|
|
| 1071 |
blurCanvas.width=16;blurCanvas.height=16;
|
| 1072 |
var blurCtx=blurCanvas.getContext('2d');
|
| 1073 |
|
| 1074 |
-
//
|
| 1075 |
-
|
| 1076 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
|
| 1078 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1079 |
|
| 1080 |
// ์งํ๋ฅ ์
๋ฐ์ดํธ
|
| 1081 |
-
|
| 1082 |
-
document.getElementById('
|
| 1083 |
-
document.getElementById('exportMsg').textContent='๋
นํ ์ค... '+pct+'% ('+t.toFixed(1)+'/'+dur.toFixed(1)+'์ด)';
|
| 1084 |
|
| 1085 |
-
//
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
|
| 1089 |
-
|
| 1090 |
-
var vc=getClipAt(t,'visual');
|
| 1091 |
if(vc){{
|
| 1092 |
var el=S.els[vc.mid];
|
| 1093 |
if(el){{
|
| 1094 |
-
// ๋น๋์ค
|
| 1095 |
if(vc.type==='video'){{
|
| 1096 |
-
var
|
| 1097 |
-
|
|
|
|
|
|
|
| 1098 |
}}
|
| 1099 |
|
| 1100 |
try{{
|
|
@@ -1102,62 +1114,51 @@ var sw=el.videoWidth||el.naturalWidth||el.width||exportW;
|
|
| 1102 |
var sh=el.videoHeight||el.naturalHeight||el.height||exportH;
|
| 1103 |
|
| 1104 |
if(fillMode==='blur'){{
|
| 1105 |
-
// โ
๋ธ๋ฌ
|
| 1106 |
frameCanvas.width=sw;
|
| 1107 |
frameCanvas.height=sh;
|
| 1108 |
frameCtx.drawImage(el,0,0,sw,sh);
|
| 1109 |
-
|
| 1110 |
-
// ๋ธ๋ฌ์ฉ ์ถ์
|
| 1111 |
blurCtx.drawImage(frameCanvas,0,0,16,16);
|
| 1112 |
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
ctx.imageSmoothingQuality='high';
|
| 1116 |
var scale=Math.max(exportW/sw,exportH/sh)*1.1;
|
| 1117 |
var nw=sw*scale,nh=sh*scale;
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
// ์ด๋ก๊ฒ
|
| 1122 |
-
ctx.fillStyle='rgba(0,0,0,0.4)';
|
| 1123 |
-
ctx.fillRect(0,0,exportW,exportH);
|
| 1124 |
-
|
| 1125 |
-
// ์๋ณธ (์บก์ฒ๋ ํ๋ ์)
|
| 1126 |
var fitScale=Math.min(exportW/sw,exportH/sh);
|
| 1127 |
var fw=sw*fitScale,fh=sh*fitScale;
|
| 1128 |
-
|
| 1129 |
-
ctx.drawImage(frameCanvas,fx,fy,fw,fh);
|
| 1130 |
}}else if(fillMode==='fill'){{
|
| 1131 |
var scale=Math.max(exportW/sw,exportH/sh);
|
| 1132 |
var nw=sw*scale,nh=sh*scale;
|
| 1133 |
-
|
| 1134 |
-
ctx.drawImage(el,ox,oy,nw,nh);
|
| 1135 |
}}else{{
|
| 1136 |
// fit
|
| 1137 |
var scale=Math.min(exportW/sw,exportH/sh);
|
| 1138 |
var nw=sw*scale,nh=sh*scale;
|
| 1139 |
-
|
| 1140 |
-
ctx.drawImage(el,ox,oy,nw,nh);
|
| 1141 |
}}
|
| 1142 |
}}catch(e){{console.error('Draw error:',e)}}
|
| 1143 |
}}
|
| 1144 |
}}
|
| 1145 |
|
| 1146 |
-
// ํ
์คํธ ์ค๋ฒ๋ ์ด ๋ ๋๋ง
|
| 1147 |
-
var textClips=getTextClipsAt(
|
| 1148 |
textClips.forEach(function(tc){{
|
| 1149 |
-
drawTextExport(
|
| 1150 |
}});
|
| 1151 |
|
| 1152 |
-
// โ
ํ
|
| 1153 |
-
|
| 1154 |
-
}}
|
| 1155 |
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
|
|
|
| 1159 |
|
| 1160 |
-
//
|
| 1161 |
Object.keys(S.els).forEach(function(k){{
|
| 1162 |
var el=S.els[k];
|
| 1163 |
if(el&&el.pause)el.pause();
|
|
@@ -1166,10 +1167,7 @@ if(el&&el.pause)el.pause();
|
|
| 1166 |
if(S.cancelled)return;
|
| 1167 |
|
| 1168 |
var webmBlob=new Blob(chunks,{{type:'video/webm'}});
|
| 1169 |
-
if(webmBlob.size<1000){{
|
| 1170 |
-
document.getElementById('exportMsg').textContent='๋
นํ ์คํจ';
|
| 1171 |
-
return;
|
| 1172 |
-
}}
|
| 1173 |
|
| 1174 |
document.getElementById('exportBar').style.width='100%';
|
| 1175 |
document.getElementById('exportMsg').textContent='์๋ฃ! ('+Math.round(webmBlob.size/1024/1024*10)/10+'MB, '+S.ratio+') - ์๋์์ ๋ค์ด๋ก๋';
|
|
@@ -1269,6 +1267,7 @@ if(initData&&initData.length)initData.forEach(function(m){{addMedia(m.name,m.typ
|
|
| 1269 |
</html>'''
|
| 1270 |
|
| 1271 |
def process_file(files):
|
|
|
|
| 1272 |
global uploaded_files
|
| 1273 |
if not files:
|
| 1274 |
return []
|
|
@@ -1289,6 +1288,7 @@ def process_file(files):
|
|
| 1289 |
else:
|
| 1290 |
continue
|
| 1291 |
|
|
|
|
| 1292 |
dst_path = os.path.join(UPLOAD_DIR, f"{int(time.time()*1000)}_{name}")
|
| 1293 |
shutil.copy(path, dst_path)
|
| 1294 |
uploaded_files[name] = dst_path
|
|
@@ -1303,20 +1303,109 @@ def make_iframe(data):
|
|
| 1303 |
h = get_editor_html(j).replace("'", "'")
|
| 1304 |
return f"<iframe srcdoc='{h}' style='width:100%;height:750px;border:none;border-radius:10px'></iframe>"
|
| 1305 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1306 |
def convert_webm_to_mp4(webm_base64):
|
|
|
|
| 1307 |
if not webm_base64 or len(webm_base64) < 100:
|
| 1308 |
return None
|
| 1309 |
|
| 1310 |
try:
|
|
|
|
| 1311 |
webm_data = base64.b64decode(webm_base64)
|
| 1312 |
|
| 1313 |
temp_dir = tempfile.mkdtemp()
|
| 1314 |
webm_path = os.path.join(temp_dir, 'input.webm')
|
| 1315 |
mp4_path = os.path.join(temp_dir, f'output_{int(time.time())}.mp4')
|
| 1316 |
|
|
|
|
| 1317 |
with open(webm_path, 'wb') as f:
|
| 1318 |
f.write(webm_data)
|
| 1319 |
|
|
|
|
| 1320 |
cmd = [
|
| 1321 |
'ffmpeg', '-y',
|
| 1322 |
'-i', webm_path,
|
|
@@ -1329,8 +1418,9 @@ def convert_webm_to_mp4(webm_base64):
|
|
| 1329 |
mp4_path
|
| 1330 |
]
|
| 1331 |
|
| 1332 |
-
subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
| 1333 |
|
|
|
|
| 1334 |
try:
|
| 1335 |
os.remove(webm_path)
|
| 1336 |
except:
|
|
|
|
| 1 |
"""
|
| 2 |
Simple Video Editor - Canvas ๊ธฐ๋ฐ ๋ ๋๋ง
|
| 3 |
+
๋น ํ๋ ์ ๋ฌธ์ ์์ ํด๊ฒฐ + ์๋ฒ ์ฌ์ด๋ MP4 ๋ด๋ณด๋ด๊ธฐ
|
| 4 |
+
๋ธ๋ฌ ๋ฐฐ๊ฒฝ ๋ด๋ณด๋ด๊ธฐ ์์ : ๋๋ธ ๋ฒํผ๋ง
|
| 5 |
"""
|
| 6 |
|
| 7 |
import gradio as gr
|
|
|
|
| 13 |
import shutil
|
| 14 |
import time
|
| 15 |
|
| 16 |
+
# ์๋ฒ ์ฌ์ด๋ MP4 ๋ด๋ณด๋ด๊ธฐ์ฉ
|
| 17 |
UPLOAD_DIR = tempfile.mkdtemp()
|
| 18 |
+
uploaded_files = {} # {filename: filepath}
|
| 19 |
|
| 20 |
def get_editor_html(media_data="[]"):
|
| 21 |
return f'''<!DOCTYPE html>
|
|
|
|
| 271 |
setupCanvasDrag();
|
| 272 |
}}
|
| 273 |
|
| 274 |
+
// ์บ๋ฒ์ค์์ ํ
์คํธ ๋๋๊ทธ ์ค์
|
| 275 |
function setupCanvasDrag(){{
|
| 276 |
var canvas=S.canvas;
|
| 277 |
var dragging=null;
|
|
|
|
| 284 |
var mx=(e.clientX-rect.left)*scaleX;
|
| 285 |
var my=(e.clientY-rect.top)*scaleY;
|
| 286 |
|
| 287 |
+
// ํ์ฌ ์๊ฐ์ ํ
์คํธ ํด๋ฆฝ ์ค ํด๋ฆญ๋ ๊ฒ ์ฐพ๊ธฐ
|
| 288 |
var textClips=getTextClipsAt(S.time);
|
| 289 |
for(var i=textClips.length-1;i>=0;i--){{
|
| 290 |
var tc=textClips[i];
|
|
|
|
| 317 |
dragging.posY=Math.max(0.1,Math.min(0.9,(my-dragOffsetY)/size.ph));
|
| 318 |
drawFrame();
|
| 319 |
}}else{{
|
| 320 |
+
// ํธ๋ฒ ์ปค์ ๋ณ๊ฒฝ
|
| 321 |
var textClips=getTextClipsAt(S.time);
|
| 322 |
var onText=false;
|
| 323 |
for(var i=0;i<textClips.length;i++){{
|
|
|
|
| 549 |
var c=S.clips.find(function(x){{return x.id===cid}});
|
| 550 |
if(c){{
|
| 551 |
c.start=r(Math.max(0,t-ox/(S.pps*S.zoom)));
|
| 552 |
+
// ํ
์คํธ๋ ํ
์คํธ ํธ๋์ผ๋ก๋ง, ๊ทธ ์ธ๋ ์์/์ค๋์ค ํธ๋์ผ๋ก
|
| 553 |
if(c.type==='text'){{
|
| 554 |
c.track=2;
|
| 555 |
}}else if(c.type==='audio'){{
|
|
|
|
| 607 |
var len=r(c.te-c.ts);
|
| 608 |
|
| 609 |
if(c.type==='text'){{
|
| 610 |
+
// ํ
์คํธ ํด๋ฆฝ ์์ฑ
|
| 611 |
box.innerHTML='<div class="prop-group"><div class="prop-label">ํ
์คํธ</div><input class="prop-input" value="'+c.text+'" onchange="setProp(\\'text\\',this.value)"></div>'+
|
| 612 |
'<div class="prop-group"><div class="prop-label">์์</div><input class="prop-input" type="number" step="0.1" value="'+c.start+'" onchange="setProp(\\'start\\',parseFloat(this.value))"></div>'+
|
| 613 |
'<div class="prop-group"><div class="prop-label">๊ธธ์ด: '+fmt(len)+'</div></div>'+
|
|
|
|
| 628 |
'</select></div>'+
|
| 629 |
'<div class="prop-group"><div class="prop-label">๋ฐฐ๊ฒฝ ์์</div><input class="prop-input" type="color" value="'+c.bgColor+'" onchange="setProp(\\'bgColor\\',this.value)"></div>';
|
| 630 |
}}else{{
|
| 631 |
+
// ๊ธฐ์กด ๋ฏธ๋์ด ํด๋ฆฝ ์์ฑ
|
| 632 |
box.innerHTML='<div class="prop-group"><div class="prop-label">์ด๋ฆ</div><input class="prop-input" value="'+c.name+'" onchange="setProp(\\'name\\',this.value)"></div>'+
|
| 633 |
'<div class="prop-group"><div class="prop-label">์์</div><input class="prop-input" type="number" step="0.1" value="'+c.start+'" onchange="setProp(\\'start\\',parseFloat(this.value))"></div>'+
|
| 634 |
'<div class="prop-group"><div class="prop-label">๊ธธ์ด: '+fmt(len)+'</div></div>'+
|
|
|
|
| 784 |
S.ctx.textAlign='center';
|
| 785 |
S.ctx.fillText('ํ์๋ผ์ธ์ ๋ฏธ๋์ด๋ฅผ ์ถ๊ฐํ์ธ์',pw/2,ph/2);
|
| 786 |
}}
|
|
|
|
| 787 |
var audioClips=S.clips.filter(function(c){{
|
| 788 |
if(c.type!=='audio')return false;
|
| 789 |
var cEnd=c.start+(c.te-c.ts);
|
|
|
|
| 817 |
}}
|
| 818 |
}}
|
| 819 |
|
| 820 |
+
// ํ
์คํธ ์ค๋ฒ๋ ์ด ๋ ๋๋ง
|
| 821 |
var textClips=getTextClipsAt(t);
|
| 822 |
textClips.forEach(function(tc){{
|
| 823 |
drawText(S.ctx,tc,pw,ph);
|
|
|
|
| 826 |
|
| 827 |
function drawWithMode(ctx,el,sw,sh,dw,dh,mode){{
|
| 828 |
if(mode==='fill'){{
|
| 829 |
+
// ์ฑ์ฐ๊ธฐ: ์บ๋ฒ์ค๋ฅผ ๊ฝ ์ฑ์ฐ๋๋ก ํ๋, ๋์น๋ ๋ถ๋ถ ์๋ฆผ
|
| 830 |
var scale=Math.max(dw/sw,dh/sh);
|
| 831 |
var nw=sw*scale,nh=sh*scale;
|
| 832 |
var ox=(dw-nw)/2,oy=(dh-nh)/2;
|
| 833 |
ctx.drawImage(el,ox,oy,nw,nh);
|
| 834 |
}}else if(mode==='blur'){{
|
| 835 |
+
// ํ๋ ์ ๋จผ์ ์บก์ฒ
|
| 836 |
var frameCanvas=document.createElement('canvas');
|
| 837 |
frameCanvas.width=sw;frameCanvas.height=sh;
|
| 838 |
var frameCtx=frameCanvas.getContext('2d');
|
| 839 |
frameCtx.drawImage(el,0,0,sw,sh);
|
| 840 |
|
| 841 |
+
// ๋ธ๋ฌ์ฉ ์์ ์บ๋ฒ์ค
|
| 842 |
var blurCanvas=document.createElement('canvas');
|
| 843 |
blurCanvas.width=16;blurCanvas.height=16;
|
| 844 |
var blurCtx=blurCanvas.getContext('2d');
|
| 845 |
blurCtx.drawImage(frameCanvas,0,0,16,16);
|
| 846 |
|
| 847 |
+
// ๋ฐฐ๊ฒฝ
|
| 848 |
ctx.imageSmoothingEnabled=true;
|
| 849 |
ctx.imageSmoothingQuality='high';
|
| 850 |
var scale=Math.max(dw/sw,dh/sh)*1.1;
|
|
|
|
| 852 |
var ox=(dw-nw)/2,oy=(dh-nh)/2;
|
| 853 |
ctx.drawImage(blurCanvas,ox,oy,nw,nh);
|
| 854 |
|
| 855 |
+
// ์ด๋ก๊ฒ
|
| 856 |
ctx.fillStyle='rgba(0,0,0,0.4)';
|
| 857 |
ctx.fillRect(0,0,dw,dh);
|
| 858 |
|
| 859 |
+
// ์๋ณธ (์บก์ฒ๋ ํ๋ ์)
|
| 860 |
var fitScale=Math.min(dw/sw,dh/sh);
|
| 861 |
var fw=sw*fitScale,fh=sh*fitScale;
|
| 862 |
var fx=(dw-fw)/2,fy=(dh-fh)/2;
|
| 863 |
ctx.drawImage(frameCanvas,fx,fy,fw,fh);
|
| 864 |
}}else{{
|
| 865 |
+
// ๋ง์ถค (fit): ์๋ณธ ๋น์จ ์ ์ง, ์ฌ๋ฐฑ
|
| 866 |
var scale=Math.min(dw/sw,dh/sh);
|
| 867 |
var nw=sw*scale,nh=sh*scale;
|
| 868 |
var ox=(dw-nw)/2,oy=(dh-nh)/2;
|
|
|
|
| 872 |
|
| 873 |
function toggleMute(){{S.muted=!S.muted;document.getElementById('muteBtn').textContent=S.muted?'๐':'๐'}}
|
| 874 |
|
| 875 |
+
// ํ
์คํธ ํด๋ฆฝ ๊ด๋ จ ํจ์๋ค
|
| 876 |
function addTextClip(){{
|
| 877 |
document.getElementById('textModal').style.display='flex';
|
| 878 |
document.getElementById('textInput').value='';
|
|
|
|
| 906 |
fontColor:document.getElementById('textColor').value,
|
| 907 |
bgStyle:document.getElementById('textBgStyle').value,
|
| 908 |
bgColor:document.getElementById('textBgColor').value,
|
| 909 |
+
posX:0.5, // 0~1 ๋น์จ (๊ฐ์ด๋ฐ)
|
| 910 |
+
posY:0.85 // 0~1 ๋น์จ (ํ๋จ)
|
| 911 |
}});
|
| 912 |
closeTextModal();
|
| 913 |
renderTL();
|
|
|
|
| 943 |
var x=dw*posX;
|
| 944 |
var y=dh*posY;
|
| 945 |
|
| 946 |
+
// ํ
์คํธ ์์ญ ์ ์ฅ (ํด๋ฆญ ๊ฐ์ง์ฉ)
|
| 947 |
clip._bounds={{
|
| 948 |
x:x-textW/2-fontSize*0.3,
|
| 949 |
y:y-textH/2-fontSize*0.15,
|
|
|
|
| 968 |
}}
|
| 969 |
}}
|
| 970 |
|
| 971 |
+
// ํ
์คํธ ๊ทธ๋ฆผ์
|
| 972 |
ctx.fillStyle='rgba(0,0,0,0.5)';
|
| 973 |
ctx.fillText(text,x+2,y+2);
|
| 974 |
|
| 975 |
+
// ํ
์คํธ
|
| 976 |
ctx.fillStyle=fontColor;
|
| 977 |
ctx.fillText(text,x,y);
|
| 978 |
|
| 979 |
+
// ์ ํ๋ ํ
์คํธ๋ฉด ํ
๋๋ฆฌ ํ์
|
| 980 |
if(S.sel===clip.id){{
|
| 981 |
ctx.strokeStyle='#6366f1';
|
| 982 |
ctx.lineWidth=2;
|
|
|
|
| 985 |
ctx.setLineDash([]);
|
| 986 |
}}
|
| 987 |
}}
|
|
|
|
| 988 |
function setZoom(v){{S.zoom=parseFloat(v);renderTL();updateHead()}}
|
| 989 |
function tlClick(e){{
|
| 990 |
if(e.target.closest('.clip'))return;
|
|
|
|
| 1009 |
}}
|
| 1010 |
|
| 1011 |
// ============================================
|
| 1012 |
+
// ํต์ฌ ์์ : ๋๋ธ ๋ฒํผ๋ง์ผ๋ก ๋ธ๋ฌ ๋ฒ์ฉ์ ๋ฐฉ์ง
|
| 1013 |
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1014 |
async function doExport(){{
|
| 1015 |
var size=S.ratioSizes[S.ratio];
|
| 1016 |
var exportW=size.w,exportH=size.h;
|
| 1017 |
|
| 1018 |
+
// โ
๋ฉ์ธ ์บ๋ฒ์ค (MediaRecorder ์ฐ๊ฒฐ) - ์์ฑ๋ ํ๋ ์๋ง ๋ฐ์
|
| 1019 |
+
var mainCanvas=document.createElement('canvas');
|
| 1020 |
+
mainCanvas.width=exportW;mainCanvas.height=exportH;
|
| 1021 |
+
var mainCtx=mainCanvas.getContext('2d');
|
| 1022 |
+
|
| 1023 |
+
// โ
์คํ์คํฌ๋ฆฐ ์บ๋ฒ์ค (๋ ๋๋ง์ฉ) - ์ฌ๊ธฐ์ ๋ชจ๋ ์์
ํ ํ๋ฒ์ ๋ณต์ฌ
|
| 1024 |
+
var offCanvas=document.createElement('canvas');
|
| 1025 |
+
offCanvas.width=exportW;offCanvas.height=exportH;
|
| 1026 |
+
var offCtx=offCanvas.getContext('2d');
|
| 1027 |
|
| 1028 |
// ์ด๊ธฐ ๊ฒ์ ํ๋ฉด
|
| 1029 |
+
mainCtx.fillStyle='#000';
|
| 1030 |
+
mainCtx.fillRect(0,0,exportW,exportH);
|
| 1031 |
|
| 1032 |
+
var stream=mainCanvas.captureStream(30);
|
| 1033 |
|
| 1034 |
+
// ๊ณ ํ์ง ์ค์
|
| 1035 |
var opts={{mimeType:'video/webm;codecs=vp9',videoBitsPerSecond:8000000}};
|
| 1036 |
if(!MediaRecorder.isTypeSupported(opts.mimeType)){{
|
| 1037 |
opts={{mimeType:'video/webm;codecs=vp8',videoBitsPerSecond:8000000}};
|
|
|
|
| 1048 |
rec.start(100);
|
| 1049 |
|
| 1050 |
var dur=S.dur;
|
|
|
|
|
|
|
| 1051 |
var fillMode=S.fillMode;
|
| 1052 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
// ๋ธ๋ฌ์ฉ ์บ๋ฒ์ค (์ฌ์ฌ์ฉ)
|
| 1054 |
var frameCanvas=document.createElement('canvas');
|
| 1055 |
var frameCtx=frameCanvas.getContext('2d');
|
|
|
|
| 1057 |
blurCanvas.width=16;blurCanvas.height=16;
|
| 1058 |
var blurCtx=blurCanvas.getContext('2d');
|
| 1059 |
|
| 1060 |
+
// ๋ชจ๋ ๋น๋์ค๋ฅผ ์ฒ์์ผ๋ก ๋๋๋ฆฌ๊ณ ์ฌ์
|
| 1061 |
+
var videoClips=S.clips.filter(function(c){{return c.type==='video'}});
|
| 1062 |
+
videoClips.forEach(function(vc){{
|
| 1063 |
+
var el=S.els[vc.mid];
|
| 1064 |
+
if(el){{
|
| 1065 |
+
el.currentTime=vc.ts;
|
| 1066 |
+
el.muted=true;
|
| 1067 |
+
}}
|
| 1068 |
+
}});
|
| 1069 |
+
|
| 1070 |
+
// ์ ์ ๋๊ธฐ ํ ์ฌ์ ์์
|
| 1071 |
+
await new Promise(function(r){{setTimeout(r,200)}});
|
| 1072 |
+
|
| 1073 |
+
videoClips.forEach(function(vc){{
|
| 1074 |
+
var el=S.els[vc.mid];
|
| 1075 |
+
if(el)el.play().catch(function(){{}});
|
| 1076 |
+
}});
|
| 1077 |
+
|
| 1078 |
+
var startTime=performance.now();
|
| 1079 |
|
| 1080 |
+
// ์ค์๊ฐ ๋ ๋๋ง ๋ฃจํ
|
| 1081 |
+
await new Promise(function(resolve){{
|
| 1082 |
+
function render(){{
|
| 1083 |
+
if(S.cancelled){{rec.stop();resolve();return}}
|
| 1084 |
+
|
| 1085 |
+
var elapsed=(performance.now()-startTime)/1000;
|
| 1086 |
+
if(elapsed>=dur){{
|
| 1087 |
+
rec.stop();
|
| 1088 |
+
setTimeout(resolve,200);
|
| 1089 |
+
return;
|
| 1090 |
+
}}
|
| 1091 |
|
| 1092 |
// ์งํ๋ฅ ์
๋ฐ์ดํธ
|
| 1093 |
+
document.getElementById('exportBar').style.width=(elapsed/dur*100)+'%';
|
| 1094 |
+
document.getElementById('exportMsg').textContent='๋
นํ ์ค... '+Math.round(elapsed/dur*100)+'%';
|
|
|
|
| 1095 |
|
| 1096 |
+
// โ
์คํ์คํฌ๋ฆฐ ์บ๋ฒ์ค์ ์์ ํ ๋ ๋๋ง โ
|
| 1097 |
+
offCtx.fillStyle='#000';
|
| 1098 |
+
offCtx.fillRect(0,0,exportW,exportH);
|
| 1099 |
|
| 1100 |
+
var vc=getClipAt(elapsed,'visual');
|
|
|
|
| 1101 |
if(vc){{
|
| 1102 |
var el=S.els[vc.mid];
|
| 1103 |
if(el){{
|
| 1104 |
+
// ๋น๋์ค ์๊ฐ ๋๊ธฐํ (์ฐจ์ด๊ฐ ํฌ๋ฉด ๋ณด์ )
|
| 1105 |
if(vc.type==='video'){{
|
| 1106 |
+
var targetT=elapsed-vc.start+vc.ts;
|
| 1107 |
+
if(Math.abs(el.currentTime-targetT)>0.5){{
|
| 1108 |
+
el.currentTime=targetT;
|
| 1109 |
+
}}
|
| 1110 |
}}
|
| 1111 |
|
| 1112 |
try{{
|
|
|
|
| 1114 |
var sh=el.videoHeight||el.naturalHeight||el.height||exportH;
|
| 1115 |
|
| 1116 |
if(fillMode==='blur'){{
|
| 1117 |
+
// โ
๋ธ๋ฌ: ์คํ์คํฌ๋ฆฐ์์ ์์ ํ ์ฒ๋ฆฌ ํ ๋ฉ์ธ์ ๋ณต์ฌ โ
|
| 1118 |
frameCanvas.width=sw;
|
| 1119 |
frameCanvas.height=sh;
|
| 1120 |
frameCtx.drawImage(el,0,0,sw,sh);
|
|
|
|
|
|
|
| 1121 |
blurCtx.drawImage(frameCanvas,0,0,16,16);
|
| 1122 |
|
| 1123 |
+
offCtx.imageSmoothingEnabled=true;
|
| 1124 |
+
offCtx.imageSmoothingQuality='high';
|
|
|
|
| 1125 |
var scale=Math.max(exportW/sw,exportH/sh)*1.1;
|
| 1126 |
var nw=sw*scale,nh=sh*scale;
|
| 1127 |
+
offCtx.drawImage(blurCanvas,(exportW-nw)/2,(exportH-nh)/2,nw,nh);
|
| 1128 |
+
offCtx.fillStyle='rgba(0,0,0,0.4)';
|
| 1129 |
+
offCtx.fillRect(0,0,exportW,exportH);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
var fitScale=Math.min(exportW/sw,exportH/sh);
|
| 1131 |
var fw=sw*fitScale,fh=sh*fitScale;
|
| 1132 |
+
offCtx.drawImage(frameCanvas,(exportW-fw)/2,(exportH-fh)/2,fw,fh);
|
|
|
|
| 1133 |
}}else if(fillMode==='fill'){{
|
| 1134 |
var scale=Math.max(exportW/sw,exportH/sh);
|
| 1135 |
var nw=sw*scale,nh=sh*scale;
|
| 1136 |
+
offCtx.drawImage(el,(exportW-nw)/2,(exportH-nh)/2,nw,nh);
|
|
|
|
| 1137 |
}}else{{
|
| 1138 |
// fit
|
| 1139 |
var scale=Math.min(exportW/sw,exportH/sh);
|
| 1140 |
var nw=sw*scale,nh=sh*scale;
|
| 1141 |
+
offCtx.drawImage(el,(exportW-nw)/2,(exportH-nh)/2,nw,nh);
|
|
|
|
| 1142 |
}}
|
| 1143 |
}}catch(e){{console.error('Draw error:',e)}}
|
| 1144 |
}}
|
| 1145 |
}}
|
| 1146 |
|
| 1147 |
+
// ํ
์คํธ ์ค๋ฒ๋ ์ด ๋ ๋๋ง (์คํ์คํฌ๋ฆฐ์)
|
| 1148 |
+
var textClips=getTextClipsAt(elapsed);
|
| 1149 |
textClips.forEach(function(tc){{
|
| 1150 |
+
drawTextExport(offCtx,tc,exportW,exportH);
|
| 1151 |
}});
|
| 1152 |
|
| 1153 |
+
// โ
์คํ์คํฌ๋ฆฐ โ ๋ฉ์ธ ์บ๋ฒ์ค๋ก ํ๋ฒ์ ๋ณต์ฌ (atomic) โ
|
| 1154 |
+
mainCtx.drawImage(offCanvas,0,0);
|
|
|
|
| 1155 |
|
| 1156 |
+
requestAnimationFrame(render);
|
| 1157 |
+
}}
|
| 1158 |
+
requestAnimationFrame(render);
|
| 1159 |
+
}});
|
| 1160 |
|
| 1161 |
+
// ์ ๋ฆฌ
|
| 1162 |
Object.keys(S.els).forEach(function(k){{
|
| 1163 |
var el=S.els[k];
|
| 1164 |
if(el&&el.pause)el.pause();
|
|
|
|
| 1167 |
if(S.cancelled)return;
|
| 1168 |
|
| 1169 |
var webmBlob=new Blob(chunks,{{type:'video/webm'}});
|
| 1170 |
+
if(webmBlob.size<1000){{document.getElementById('exportMsg').textContent='๋
นํ ์คํจ';return}}
|
|
|
|
|
|
|
|
|
|
| 1171 |
|
| 1172 |
document.getElementById('exportBar').style.width='100%';
|
| 1173 |
document.getElementById('exportMsg').textContent='์๋ฃ! ('+Math.round(webmBlob.size/1024/1024*10)/10+'MB, '+S.ratio+') - ์๋์์ ๋ค์ด๋ก๋';
|
|
|
|
| 1267 |
</html>'''
|
| 1268 |
|
| 1269 |
def process_file(files):
|
| 1270 |
+
"""ํ์ผ ์ฒ๋ฆฌ ๋ฐ ์๋ฒ์ ์ ์ฅ"""
|
| 1271 |
global uploaded_files
|
| 1272 |
if not files:
|
| 1273 |
return []
|
|
|
|
| 1288 |
else:
|
| 1289 |
continue
|
| 1290 |
|
| 1291 |
+
# ์๋ฒ์ ํ์ผ ๋ณต์ฌ (MP4 ๋ด๋ณด๋ด๊ธฐ์ฉ)
|
| 1292 |
dst_path = os.path.join(UPLOAD_DIR, f"{int(time.time()*1000)}_{name}")
|
| 1293 |
shutil.copy(path, dst_path)
|
| 1294 |
uploaded_files[name] = dst_path
|
|
|
|
| 1303 |
h = get_editor_html(j).replace("'", "'")
|
| 1304 |
return f"<iframe srcdoc='{h}' style='width:100%;height:750px;border:none;border-radius:10px'></iframe>"
|
| 1305 |
|
| 1306 |
+
def export_mp4(export_json):
|
| 1307 |
+
"""์๋ฒ ์ฌ์ด๋ MP4 ๋ด๋ณด๋ด๊ธฐ"""
|
| 1308 |
+
global uploaded_files
|
| 1309 |
+
|
| 1310 |
+
if not export_json or len(export_json) < 10:
|
| 1311 |
+
return None
|
| 1312 |
+
|
| 1313 |
+
try:
|
| 1314 |
+
data = json.loads(export_json)
|
| 1315 |
+
clips = data.get('clips', [])
|
| 1316 |
+
|
| 1317 |
+
if not clips:
|
| 1318 |
+
return None
|
| 1319 |
+
|
| 1320 |
+
video_clips = [c for c in clips if c['type'] in ['video', 'image']]
|
| 1321 |
+
if not video_clips:
|
| 1322 |
+
return None
|
| 1323 |
+
|
| 1324 |
+
temp_dir = tempfile.mkdtemp()
|
| 1325 |
+
output_path = os.path.join(temp_dir, f'output_{int(time.time())}.mp4')
|
| 1326 |
+
|
| 1327 |
+
# ๋จ์ผ ํด๋ฆฝ
|
| 1328 |
+
if len(video_clips) == 1:
|
| 1329 |
+
clip = video_clips[0]
|
| 1330 |
+
file_path = uploaded_files.get(clip['filePath'])
|
| 1331 |
+
|
| 1332 |
+
if not file_path or not os.path.exists(file_path):
|
| 1333 |
+
return None
|
| 1334 |
+
|
| 1335 |
+
duration = clip['te'] - clip['ts']
|
| 1336 |
+
|
| 1337 |
+
if clip['type'] == 'image':
|
| 1338 |
+
cmd = ['ffmpeg', '-y', '-loop', '1', '-i', file_path, '-c:v', 'libx264', '-t', str(duration), '-pix_fmt', 'yuv420p', '-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2', output_path]
|
| 1339 |
+
else:
|
| 1340 |
+
cmd = ['ffmpeg', '-y', '-i', file_path, '-ss', str(clip['ts']), '-t', str(duration), '-c:v', 'libx264', '-preset', 'fast', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', '-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2', '-movflags', '+faststart', output_path]
|
| 1341 |
+
|
| 1342 |
+
subprocess.run(cmd, capture_output=True, timeout=300)
|
| 1343 |
+
|
| 1344 |
+
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
| 1345 |
+
return output_path
|
| 1346 |
+
return None
|
| 1347 |
+
|
| 1348 |
+
# ์ฌ๋ฌ ํด๋ฆฝ
|
| 1349 |
+
temp_files = []
|
| 1350 |
+
concat_file = os.path.join(temp_dir, 'concat.txt')
|
| 1351 |
+
|
| 1352 |
+
for i, clip in enumerate(sorted(video_clips, key=lambda x: x['start'])):
|
| 1353 |
+
file_path = uploaded_files.get(clip['filePath'])
|
| 1354 |
+
if not file_path or not os.path.exists(file_path):
|
| 1355 |
+
continue
|
| 1356 |
+
|
| 1357 |
+
temp_out = os.path.join(temp_dir, f'temp_{i}.mp4')
|
| 1358 |
+
duration = clip['te'] - clip['ts']
|
| 1359 |
+
|
| 1360 |
+
if clip['type'] == 'image':
|
| 1361 |
+
cmd = ['ffmpeg', '-y', '-loop', '1', '-i', file_path, '-c:v', 'libx264', '-t', str(duration), '-pix_fmt', 'yuv420p', '-r', '30', '-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2', temp_out]
|
| 1362 |
+
else:
|
| 1363 |
+
cmd = ['ffmpeg', '-y', '-i', file_path, '-ss', str(clip['ts']), '-t', str(duration), '-c:v', 'libx264', '-preset', 'fast', '-c:a', 'aac', '-r', '30', '-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2', temp_out]
|
| 1364 |
+
|
| 1365 |
+
subprocess.run(cmd, capture_output=True, timeout=120)
|
| 1366 |
+
if os.path.exists(temp_out):
|
| 1367 |
+
temp_files.append(temp_out)
|
| 1368 |
+
|
| 1369 |
+
if not temp_files:
|
| 1370 |
+
return None
|
| 1371 |
+
|
| 1372 |
+
with open(concat_file, 'w') as f:
|
| 1373 |
+
for tf in temp_files:
|
| 1374 |
+
f.write(f"file '{tf}'\n")
|
| 1375 |
+
|
| 1376 |
+
cmd = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_file, '-c:v', 'libx264', '-preset', 'fast', '-c:a', 'aac', '-movflags', '+faststart', output_path]
|
| 1377 |
+
subprocess.run(cmd, capture_output=True, timeout=300)
|
| 1378 |
+
|
| 1379 |
+
for tf in temp_files:
|
| 1380 |
+
try: os.remove(tf)
|
| 1381 |
+
except: pass
|
| 1382 |
+
|
| 1383 |
+
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
| 1384 |
+
return output_path
|
| 1385 |
+
return None
|
| 1386 |
+
|
| 1387 |
+
except Exception as e:
|
| 1388 |
+
print(f"[Export] Error: {e}")
|
| 1389 |
+
return None
|
| 1390 |
+
|
| 1391 |
def convert_webm_to_mp4(webm_base64):
|
| 1392 |
+
"""WebM base64 ๋ฐ์ดํฐ๋ฅผ MP4๋ก ๋ณํ"""
|
| 1393 |
if not webm_base64 or len(webm_base64) < 100:
|
| 1394 |
return None
|
| 1395 |
|
| 1396 |
try:
|
| 1397 |
+
# base64 ๋์ฝ๋ฉ
|
| 1398 |
webm_data = base64.b64decode(webm_base64)
|
| 1399 |
|
| 1400 |
temp_dir = tempfile.mkdtemp()
|
| 1401 |
webm_path = os.path.join(temp_dir, 'input.webm')
|
| 1402 |
mp4_path = os.path.join(temp_dir, f'output_{int(time.time())}.mp4')
|
| 1403 |
|
| 1404 |
+
# WebM ํ์ผ ์ ์ฅ
|
| 1405 |
with open(webm_path, 'wb') as f:
|
| 1406 |
f.write(webm_data)
|
| 1407 |
|
| 1408 |
+
# FFmpeg๋ก MP4 ๋ณํ
|
| 1409 |
cmd = [
|
| 1410 |
'ffmpeg', '-y',
|
| 1411 |
'-i', webm_path,
|
|
|
|
| 1418 |
mp4_path
|
| 1419 |
]
|
| 1420 |
|
| 1421 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
| 1422 |
|
| 1423 |
+
# WebM ํ์ผ ์ญ์
|
| 1424 |
try:
|
| 1425 |
os.remove(webm_path)
|
| 1426 |
except:
|