seawolf2357 commited on
Commit
12b1424
ยท
verified ยท
1 Parent(s): 3246982

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -103
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 canvas=document.createElement('canvas');
1032
- canvas.width=exportW;canvas.height=exportH;
1033
- var ctx=canvas.getContext('2d');
 
 
 
 
 
1034
 
1035
  // ์ดˆ๊ธฐ ๊ฒ€์€ ํ™”๋ฉด
1036
- ctx.fillStyle='#000';
1037
- ctx.fillRect(0,0,exportW,exportH);
1038
 
1039
- var stream=canvas.captureStream(30);
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
- for(var frame=0;frame<=totalFrames;frame++){{
1076
- if(S.cancelled)break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
 
1078
- var t=frame/fps;
 
 
 
 
 
 
 
 
 
 
1079
 
1080
  // ์ง„ํ–‰๋ฅ  ์—…๋ฐ์ดํŠธ
1081
- var pct=Math.round(frame/totalFrames*100);
1082
- document.getElementById('exportBar').style.width=pct+'%';
1083
- document.getElementById('exportMsg').textContent='๋…นํ™” ์ค‘... '+pct+'% ('+t.toFixed(1)+'/'+dur.toFixed(1)+'์ดˆ)';
1084
 
1085
- // ๊ฒ€์€ ๋ฐฐ๊ฒฝ
1086
- ctx.fillStyle='#000';
1087
- ctx.fillRect(0,0,exportW,exportH);
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 clipT=t-vc.start+vc.ts;
1097
- await seekVideoAndWait(el,clipT);
 
 
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
- ctx.imageSmoothingEnabled=true;
1115
- ctx.imageSmoothingQuality='high';
1116
  var scale=Math.max(exportW/sw,exportH/sh)*1.1;
1117
  var nw=sw*scale,nh=sh*scale;
1118
- var ox=(exportW-nw)/2,oy=(exportH-nh)/2;
1119
- ctx.drawImage(blurCanvas,ox,oy,nw,nh);
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
- var fx=(exportW-fw)/2,fy=(exportH-fh)/2;
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
- var ox=(exportW-nw)/2,oy=(exportH-nh)/2;
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
- var ox=(exportW-nw)/2,oy=(exportH-nh)/2;
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(t);
1148
  textClips.forEach(function(tc){{
1149
- drawTextExport(ctx,tc,exportW,exportH);
1150
  }});
1151
 
1152
- // โ˜… ํ”„๋ ˆ์ž„ ๊ฐ„๊ฒฉ ๋Œ€๊ธฐ - MediaRecorder๊ฐ€ ํ”„๋ ˆ์ž„ ์บก์ฒ˜ํ•  ์‹œ๊ฐ„ โ˜…
1153
- await sleep(1000/fps);
1154
- }}
1155
 
1156
- // ๋…นํ™” ์ข…๋ฃŒ
1157
- rec.stop();
1158
- await sleep(300);
 
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("'", "&#39;")
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("'", "&#39;")
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: