Spaces:
Running
Running
Upload 4 files
Browse files- .gitattributes +1 -0
- Dockerfile +4 -0
- NotoSansMyanmar-Bold.ttf +3 -0
- app.py +93 -13
- index.html +154 -0
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
NotoSansMyanmar-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
CHANGED
|
@@ -3,6 +3,9 @@ FROM python:3.11-slim
|
|
| 3 |
# System deps + deno (yt-dlp JS challenge solver)
|
| 4 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
ffmpeg git curl unzip \
|
|
|
|
|
|
|
|
|
|
| 6 |
&& curl -fsSL https://deno.land/install.sh | sh \
|
| 7 |
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
|
|
@@ -28,6 +31,7 @@ RUN pip install --no-cache-dir -U "yt-dlp[default]"
|
|
| 28 |
COPY app.py .
|
| 29 |
COPY bot.py .
|
| 30 |
COPY index.html .
|
|
|
|
| 31 |
COPY m_youtube_com_cookies.txt .
|
| 32 |
COPY start.sh .
|
| 33 |
RUN chmod +x start.sh
|
|
|
|
| 3 |
# System deps + deno (yt-dlp JS challenge solver)
|
| 4 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
ffmpeg git curl unzip \
|
| 6 |
+
libass9 libass-dev \
|
| 7 |
+
fonts-noto fonts-noto-cjk \
|
| 8 |
+
&& fc-cache -fv \
|
| 9 |
&& curl -fsSL https://deno.land/install.sh | sh \
|
| 10 |
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
|
|
|
| 31 |
COPY app.py .
|
| 32 |
COPY bot.py .
|
| 33 |
COPY index.html .
|
| 34 |
+
COPY NotoSansMyanmar-Bold.ttf .
|
| 35 |
COPY m_youtube_com_cookies.txt .
|
| 36 |
COPY start.sh .
|
| 37 |
RUN chmod +x start.sh
|
NotoSansMyanmar-Bold.ttf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0f20a5e23dfa2efd0fd98b384d8c1c6ab379810985b79e0ae4369936dec6897b
|
| 3 |
+
size 209068
|
app.py
CHANGED
|
@@ -614,6 +614,58 @@ def api_draft():
|
|
| 614 |
except Exception as e:
|
| 615 |
return jsonify(ok=False, msg=f'β {e}')
|
| 616 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
# ββ #7: Audio filter β louder, cleaner voice (no hiss/air noise) ββ
|
| 618 |
def _build_audio_filter(mpath, ad):
|
| 619 |
"""
|
|
@@ -632,7 +684,7 @@ def _build_audio_filter(mpath, ad):
|
|
| 632 |
return f'[1:a]{voice_chain}[outa]'
|
| 633 |
|
| 634 |
# ββ #6: Video render β smaller output file ββ
|
| 635 |
-
def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file):
|
| 636 |
raw_ratio = ad / vd
|
| 637 |
|
| 638 |
# ββ Step 1: Pre-process video β resize + fix even dims using -vf (no filter_complex quoting issues) ββ
|
|
@@ -692,11 +744,19 @@ def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file):
|
|
| 692 |
else:
|
| 693 |
v_layout = f'[0:v]{base_str}'
|
| 694 |
|
|
|
|
| 695 |
if wmk:
|
| 696 |
cn = wmk.replace("'","").replace("\\","").replace(":","")
|
| 697 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
else:
|
| 699 |
-
vff = f
|
| 700 |
|
| 701 |
af = _build_audio_filter(mpath, ad)
|
| 702 |
|
|
@@ -729,12 +789,16 @@ def api_process():
|
|
| 729 |
voice_id = request.form.get('voice', 'my-MM-ThihaNeural')
|
| 730 |
engine = request.form.get('engine', 'ms')
|
| 731 |
spd = int(request.form.get('speed', 30))
|
| 732 |
-
wmk
|
| 733 |
-
crop
|
| 734 |
-
flip
|
| 735 |
-
col
|
| 736 |
-
|
| 737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
|
| 739 |
if not u: return jsonify(ok=False, msg='β Not logged in')
|
| 740 |
if not sc: return jsonify(ok=False, msg='β No script')
|
|
@@ -785,7 +849,13 @@ def api_process():
|
|
| 785 |
if vd <= 0: raise Exception('Video duration read failed')
|
| 786 |
if ad <= 0: raise Exception('Audio duration read failed')
|
| 787 |
|
| 788 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
|
| 790 |
rem = -1
|
| 791 |
if not is_adm: _, rem = deduct(u, 1); upd_stat(u, 'vd')
|
|
@@ -831,8 +901,12 @@ def api_process_all():
|
|
| 831 |
wmk = request.form.get('watermark', '')
|
| 832 |
crop = request.form.get('crop', '9:16')
|
| 833 |
flip = request.form.get('flip', '0') == '1'
|
| 834 |
-
col
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
api = request.form.get('ai_model', 'Gemini')
|
| 837 |
video_file = request.files.get('video_file')
|
| 838 |
music_file = request.files.get('music_file')
|
|
@@ -909,7 +983,13 @@ def api_process_all():
|
|
| 909 |
if vd <= 0: raise Exception('Video duration read failed')
|
| 910 |
if ad <= 0: raise Exception('Audio duration read failed')
|
| 911 |
|
| 912 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
|
| 914 |
rem = -1
|
| 915 |
if not is_adm:
|
|
|
|
| 614 |
except Exception as e:
|
| 615 |
return jsonify(ok=False, msg=f'β {e}')
|
| 616 |
|
| 617 |
+
|
| 618 |
+
# ββ SUBTITLE HELPERS ββ
|
| 619 |
+
FONT_PATH = str(BASE_DIR / 'NotoSansMyanmar-Bold.ttf')
|
| 620 |
+
|
| 621 |
+
def _srt_timestamp(s):
|
| 622 |
+
ms = int(round((s % 1) * 1000))
|
| 623 |
+
s = int(s)
|
| 624 |
+
h, rem = divmod(s, 3600)
|
| 625 |
+
m, sec = divmod(rem, 60)
|
| 626 |
+
return f"{h:02d}:{m:02d}:{sec:02d},{ms:03d}"
|
| 627 |
+
|
| 628 |
+
def _build_srt(sentences, parts, srt_path, silence_dur=0.4):
|
| 629 |
+
"""TTS parts + sentences β SRT timing file"""
|
| 630 |
+
tts_files = [p for p in parts
|
| 631 |
+
if 'sil' not in os.path.basename(p)
|
| 632 |
+
and 'silence' not in os.path.basename(p)]
|
| 633 |
+
lines = []
|
| 634 |
+
t = 0.0
|
| 635 |
+
for i, sent in enumerate(sentences):
|
| 636 |
+
fdur = dur(tts_files[i]) if i < len(tts_files) else 2.0
|
| 637 |
+
if fdur <= 0: fdur = 2.0
|
| 638 |
+
lines.append(str(i + 1))
|
| 639 |
+
lines.append(f"{_srt_timestamp(t)} --> {_srt_timestamp(t + fdur)}")
|
| 640 |
+
lines.append(sent)
|
| 641 |
+
lines.append('')
|
| 642 |
+
t += fdur + silence_dur
|
| 643 |
+
with open(srt_path, 'w', encoding='utf-8') as f:
|
| 644 |
+
f.write('\n'.join(lines))
|
| 645 |
+
|
| 646 |
+
def _subtitle_filter(srt_path, align=2, margin_v=60, fontsize=20):
|
| 647 |
+
"""
|
| 648 |
+
Yellow bold Myanmar subtitle + dark rounded box background.
|
| 649 |
+
align : ASS alignment (2=bottom-center, 5=middle, 8=top-center)
|
| 650 |
+
margin_v: pixel distance from edge (top or bottom depending on align)
|
| 651 |
+
fontsize: font size in points
|
| 652 |
+
"""
|
| 653 |
+
safe_path = srt_path.replace("'", "")
|
| 654 |
+
font_opt = ''
|
| 655 |
+
if os.path.exists(FONT_PATH):
|
| 656 |
+
safe_font = FONT_PATH.replace("'", "").replace(":", "\\:")
|
| 657 |
+
font_opt = f":fontsdir='{os.path.dirname(safe_font)}'"
|
| 658 |
+
style = (
|
| 659 |
+
f"Fontsize={fontsize},Bold=1,"
|
| 660 |
+
f"PrimaryColour=&H00FFFF00," # yellow
|
| 661 |
+
f"BackColour=&HAA000000," # dark semi-transparent box
|
| 662 |
+
f"BorderStyle=4," # filled box mode
|
| 663 |
+
f"Outline=0,Shadow=0,"
|
| 664 |
+
f"MarginV={margin_v},"
|
| 665 |
+
f"Alignment={align}"
|
| 666 |
+
)
|
| 667 |
+
return f"subtitles='{safe_path}'{font_opt}:force_style='{style}'"
|
| 668 |
+
|
| 669 |
# ββ #7: Audio filter β louder, cleaner voice (no hiss/air noise) ββ
|
| 670 |
def _build_audio_filter(mpath, ad):
|
| 671 |
"""
|
|
|
|
| 684 |
return f'[1:a]{voice_chain}[outa]'
|
| 685 |
|
| 686 |
# ββ #6: Video render β smaller output file ββ
|
| 687 |
+
def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file, sentences=None, parts=None, subtitle=False, srt_path=None, sub_align=2, sub_margin=60, sub_fontsize=20):
|
| 688 |
raw_ratio = ad / vd
|
| 689 |
|
| 690 |
# ββ Step 1: Pre-process video β resize + fix even dims using -vf (no filter_complex quoting issues) ββ
|
|
|
|
| 744 |
else:
|
| 745 |
v_layout = f'[0:v]{base_str}'
|
| 746 |
|
| 747 |
+
# ββ Watermark ββ
|
| 748 |
if wmk:
|
| 749 |
cn = wmk.replace("'","").replace("\\","").replace(":","")
|
| 750 |
+
base_vff = f"{v_layout},drawtext=text='{cn}':x=w-tw-40:y=40:fontsize=35:fontcolor=white:shadowcolor=black:shadowx=2:shadowy=2"
|
| 751 |
+
else:
|
| 752 |
+
base_vff = v_layout
|
| 753 |
+
|
| 754 |
+
# ββ Subtitle burn-in ββ
|
| 755 |
+
if subtitle and srt_path and os.path.exists(srt_path):
|
| 756 |
+
sub_f = _subtitle_filter(srt_path, align=sub_align, margin_v=sub_margin, fontsize=sub_fontsize)
|
| 757 |
+
vff = f"{base_vff},{sub_f}[outv]"
|
| 758 |
else:
|
| 759 |
+
vff = f"{base_vff}[outv]"
|
| 760 |
|
| 761 |
af = _build_audio_filter(mpath, ad)
|
| 762 |
|
|
|
|
| 789 |
voice_id = request.form.get('voice', 'my-MM-ThihaNeural')
|
| 790 |
engine = request.form.get('engine', 'ms')
|
| 791 |
spd = int(request.form.get('speed', 30))
|
| 792 |
+
wmk = request.form.get('watermark', '')
|
| 793 |
+
crop = request.form.get('crop', '9:16')
|
| 794 |
+
flip = request.form.get('flip', '0') == '1'
|
| 795 |
+
col = request.form.get('color', '0') == '1'
|
| 796 |
+
sub = request.form.get('subtitle', '0') == '1'
|
| 797 |
+
sub_align = int(request.form.get('sub_align', 2))
|
| 798 |
+
sub_margin = int(request.form.get('sub_margin', 60))
|
| 799 |
+
sub_fontsize= int(request.form.get('sub_fontsize', 20))
|
| 800 |
+
video_file = request.files.get('video_file')
|
| 801 |
+
music_file = request.files.get('music_file')
|
| 802 |
|
| 803 |
if not u: return jsonify(ok=False, msg='β Not logged in')
|
| 804 |
if not sc: return jsonify(ok=False, msg='β No script')
|
|
|
|
| 849 |
if vd <= 0: raise Exception('Video duration read failed')
|
| 850 |
if ad <= 0: raise Exception('Audio duration read failed')
|
| 851 |
|
| 852 |
+
srt_path = None
|
| 853 |
+
if sub:
|
| 854 |
+
srt_path = f'{tmp_dir}/sub.srt'
|
| 855 |
+
_build_srt(sentences, parts, srt_path)
|
| 856 |
+
_build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
|
| 857 |
+
sentences=sentences, parts=parts, subtitle=sub, srt_path=srt_path,
|
| 858 |
+
sub_align=sub_align, sub_margin=sub_margin, sub_fontsize=sub_fontsize)
|
| 859 |
|
| 860 |
rem = -1
|
| 861 |
if not is_adm: _, rem = deduct(u, 1); upd_stat(u, 'vd')
|
|
|
|
| 901 |
wmk = request.form.get('watermark', '')
|
| 902 |
crop = request.form.get('crop', '9:16')
|
| 903 |
flip = request.form.get('flip', '0') == '1'
|
| 904 |
+
col = request.form.get('color', '0') == '1'
|
| 905 |
+
sub = request.form.get('subtitle', '0') == '1'
|
| 906 |
+
sub_align = int(request.form.get('sub_align', 2))
|
| 907 |
+
sub_margin = int(request.form.get('sub_margin', 60))
|
| 908 |
+
sub_fontsize= int(request.form.get('sub_fontsize', 20))
|
| 909 |
+
ct = request.form.get('content_type', 'Movie Recap')
|
| 910 |
api = request.form.get('ai_model', 'Gemini')
|
| 911 |
video_file = request.files.get('video_file')
|
| 912 |
music_file = request.files.get('music_file')
|
|
|
|
| 983 |
if vd <= 0: raise Exception('Video duration read failed')
|
| 984 |
if ad <= 0: raise Exception('Audio duration read failed')
|
| 985 |
|
| 986 |
+
srt_path = None
|
| 987 |
+
if sub:
|
| 988 |
+
srt_path = f'{tmp_dir}/sub.srt'
|
| 989 |
+
_build_srt(sentences, parts, srt_path)
|
| 990 |
+
_build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
|
| 991 |
+
sentences=sentences, parts=parts, subtitle=sub, srt_path=srt_path,
|
| 992 |
+
sub_align=sub_align, sub_margin=sub_margin, sub_fontsize=sub_fontsize)
|
| 993 |
|
| 994 |
rem = -1
|
| 995 |
if not is_adm:
|
index.html
CHANGED
|
@@ -197,6 +197,21 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
|
|
| 197 |
.spinning{animation:spin .8s linear infinite}
|
| 198 |
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
| 199 |
.fade-in{animation:fadeIn .3s ease forwards}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
</style>
|
| 201 |
</head>
|
| 202 |
<body>
|
|
@@ -320,7 +335,32 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
|
|
| 320 |
<div class="checks-grid">
|
| 321 |
<div class="check-item" id="chk-fl" onclick="togCheck(this,'fl')"><div class="check-box"></div><span><i class="fas fa-arrows-alt-h" style="color:var(--green)"></i> Flip Video</span></div>
|
| 322 |
<div class="check-item" id="chk-ac" onclick="togCheck(this,'ac')"><div class="check-box"></div><span><i class="fas fa-magic" style="color:var(--violet)"></i> Auto Color</span></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
</div>
|
|
|
|
| 324 |
</div>
|
| 325 |
|
| 326 |
<!-- SETTINGS -->
|
|
@@ -390,6 +430,7 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
|
|
| 390 |
<div class="preview-top"><i class="fas fa-play-circle"></i> PREVIEW</div>
|
| 391 |
<div class="video-wrap">
|
| 392 |
<video id="preview-video" controls style="display:none"></video>
|
|
|
|
| 393 |
<div class="video-placeholder" id="video-placeholder"><i class="fas fa-film"></i><p>Preview will appear here</p></div>
|
| 394 |
</div>
|
| 395 |
<div class="preview-bottom">
|
|
@@ -688,6 +729,15 @@ function buildFormData(includeScript){
|
|
| 688 |
}
|
| 689 |
const mf = document.getElementById('music-file').files[0];
|
| 690 |
if(mf) fd.append('music_file', mf);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
if(includeScript){
|
| 692 |
const sc = document.getElementById('script-in').value.trim();
|
| 693 |
if(sc) fd.append('script', sc);
|
|
@@ -879,6 +929,110 @@ async function loadUsers(){
|
|
| 879 |
</table>`;
|
| 880 |
} catch(e){ wrap.innerHTML='<div style="color:var(--red);font-size:.8rem">Error: '+e+'</div>'; }
|
| 881 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
</script>
|
| 883 |
</body>
|
| 884 |
</html>
|
|
|
|
| 197 |
.spinning{animation:spin .8s linear infinite}
|
| 198 |
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
| 199 |
.fade-in{animation:fadeIn .3s ease forwards}
|
| 200 |
+
|
| 201 |
+
/* ββ SUBTITLE SETTINGS ββ */
|
| 202 |
+
.sub-settings{display:none;margin-top:10px;padding:12px;background:var(--bg3);border:1px solid var(--border);border-radius:8px}
|
| 203 |
+
.sub-settings.visible{display:block}
|
| 204 |
+
.sub-row{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
| 205 |
+
.sub-row:last-child{margin-bottom:0}
|
| 206 |
+
.sub-label{font-size:.72rem;color:var(--muted);white-space:nowrap;min-width:72px;font-weight:600}
|
| 207 |
+
.sub-val{font-size:.72rem;color:var(--amber2);font-weight:600;min-width:30px;text-align:right}
|
| 208 |
+
.sub-pos-btns{display:flex;gap:5px;flex:1}
|
| 209 |
+
.sub-pos-btn{flex:1;padding:5px 4px;background:var(--bg);border:1px solid var(--border);border-radius:5px;color:var(--muted);font-size:.7rem;font-weight:600;cursor:pointer;transition:.2s;text-align:center}
|
| 210 |
+
.sub-pos-btn.active{border-color:var(--amber);background:rgba(245,166,35,.08);color:var(--amber2)}
|
| 211 |
+
|
| 212 |
+
/* subtitle canvas overlay */
|
| 213 |
+
.video-wrap{position:relative}
|
| 214 |
+
#sub-canvas{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10}
|
| 215 |
</style>
|
| 216 |
</head>
|
| 217 |
<body>
|
|
|
|
| 335 |
<div class="checks-grid">
|
| 336 |
<div class="check-item" id="chk-fl" onclick="togCheck(this,'fl')"><div class="check-box"></div><span><i class="fas fa-arrows-alt-h" style="color:var(--green)"></i> Flip Video</span></div>
|
| 337 |
<div class="check-item" id="chk-ac" onclick="togCheck(this,'ac')"><div class="check-box"></div><span><i class="fas fa-magic" style="color:var(--violet)"></i> Auto Color</span></div>
|
| 338 |
+
<div class="check-item" id="chk-sub" onclick="togSubtitle(this)" style="grid-column:1/-1"><div class="check-box"></div><span><i class="fas fa-closed-captioning" style="color:var(--cyan)"></i> Subtitle (α
α¬αααΊααα―αΈ)</span></div>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
|
| 342 |
+
<!-- SUBTITLE SETTINGS -->
|
| 343 |
+
<div class="card" id="sub-settings-card" style="display:none">
|
| 344 |
+
<div class="card-label"><i class="fas fa-closed-captioning" style="color:var(--cyan)"></i> SUBTITLE POSITION</div>
|
| 345 |
+
<div class="sub-row">
|
| 346 |
+
<span class="sub-label"><i class="fas fa-arrows-alt-v"></i> Position</span>
|
| 347 |
+
<div class="sub-pos-btns">
|
| 348 |
+
<div class="sub-pos-btn active" id="spos-bottom" onclick="setSubPos('bottom')">β¬ Bottom</div>
|
| 349 |
+
<div class="sub-pos-btn" id="spos-middle" onclick="setSubPos('middle')">β¬ Middle</div>
|
| 350 |
+
<div class="sub-pos-btn" id="spos-top" onclick="setSubPos('top')">β¬ Top</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="sub-row">
|
| 354 |
+
<span class="sub-label"><i class="fas fa-sort-numeric-up"></i> Margin</span>
|
| 355 |
+
<input type="range" id="sub-margin" min="10" max="200" value="60" oninput="updateSubPreview()">
|
| 356 |
+
<span class="sub-val" id="sub-margin-val">60px</span>
|
| 357 |
+
</div>
|
| 358 |
+
<div class="sub-row">
|
| 359 |
+
<span class="sub-label"><i class="fas fa-text-height"></i> Font Size</span>
|
| 360 |
+
<input type="range" id="sub-fontsize" min="12" max="40" value="20" oninput="updateSubPreview()">
|
| 361 |
+
<span class="sub-val" id="sub-fontsize-val">20</span>
|
| 362 |
</div>
|
| 363 |
+
<div style="font-size:.7rem;color:var(--muted2);margin-top:6px;text-align:center">π Preview panel αα±α«αΊααΎα¬ sample text αα±α«αΊαααΊ β position αα»αααΊαα«</div>
|
| 364 |
</div>
|
| 365 |
|
| 366 |
<!-- SETTINGS -->
|
|
|
|
| 430 |
<div class="preview-top"><i class="fas fa-play-circle"></i> PREVIEW</div>
|
| 431 |
<div class="video-wrap">
|
| 432 |
<video id="preview-video" controls style="display:none"></video>
|
| 433 |
+
<canvas id="sub-canvas" style="display:none"></canvas>
|
| 434 |
<div class="video-placeholder" id="video-placeholder"><i class="fas fa-film"></i><p>Preview will appear here</p></div>
|
| 435 |
</div>
|
| 436 |
<div class="preview-bottom">
|
|
|
|
| 729 |
}
|
| 730 |
const mf = document.getElementById('music-file').files[0];
|
| 731 |
if(mf) fd.append('music_file', mf);
|
| 732 |
+
// subtitle params
|
| 733 |
+
const subOn = document.getElementById('chk-sub').classList.contains('checked');
|
| 734 |
+
fd.append('subtitle', subOn ? '1' : '0');
|
| 735 |
+
if(subOn){
|
| 736 |
+
const posMap = {bottom:'2', middle:'5', top:'8'};
|
| 737 |
+
fd.append('sub_align', posMap[SUB_POS] || '2');
|
| 738 |
+
fd.append('sub_margin', document.getElementById('sub-margin').value);
|
| 739 |
+
fd.append('sub_fontsize', document.getElementById('sub-fontsize').value);
|
| 740 |
+
}
|
| 741 |
if(includeScript){
|
| 742 |
const sc = document.getElementById('script-in').value.trim();
|
| 743 |
if(sc) fd.append('script', sc);
|
|
|
|
| 929 |
</table>`;
|
| 930 |
} catch(e){ wrap.innerHTML='<div style="color:var(--red);font-size:.8rem">Error: '+e+'</div>'; }
|
| 931 |
}
|
| 932 |
+
|
| 933 |
+
// οΏ½οΏ½β SUBTITLE SETTINGS ββ
|
| 934 |
+
let SUB_POS = 'bottom'; // 'top' | 'middle' | 'bottom'
|
| 935 |
+
|
| 936 |
+
function togSubtitle(el){
|
| 937 |
+
el.classList.toggle('checked');
|
| 938 |
+
el.querySelector('.check-box').innerHTML = el.classList.contains('checked') ? '<i class="fas fa-check"></i>' : '';
|
| 939 |
+
const on = el.classList.contains('checked');
|
| 940 |
+
document.getElementById('sub-settings-card').style.display = on ? '' : 'none';
|
| 941 |
+
const canvas = document.getElementById('sub-canvas');
|
| 942 |
+
canvas.style.display = on ? 'block' : 'none';
|
| 943 |
+
if(on) updateSubPreview(); else clearSubCanvas();
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
function setSubPos(pos){
|
| 947 |
+
SUB_POS = pos;
|
| 948 |
+
['bottom','middle','top'].forEach(p=>{
|
| 949 |
+
document.getElementById('spos-'+p).classList.toggle('active', p===pos);
|
| 950 |
+
});
|
| 951 |
+
// reset margin to sensible default per position
|
| 952 |
+
const defaults = {bottom:60, middle:0, top:60};
|
| 953 |
+
document.getElementById('sub-margin').value = defaults[pos];
|
| 954 |
+
updateSubPreview();
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
function updateSubPreview(){
|
| 958 |
+
const margin = parseInt(document.getElementById('sub-margin').value);
|
| 959 |
+
const fontSize = parseInt(document.getElementById('sub-fontsize').value);
|
| 960 |
+
document.getElementById('sub-margin-val').textContent = margin+'px';
|
| 961 |
+
document.getElementById('sub-fontsize-val').textContent = fontSize;
|
| 962 |
+
drawSubCanvas(margin, fontSize);
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
function clearSubCanvas(){
|
| 966 |
+
const c = document.getElementById('sub-canvas');
|
| 967 |
+
const ctx = c.getContext('2d');
|
| 968 |
+
ctx.clearRect(0, 0, c.width, c.height);
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
function drawSubCanvas(margin, fontSize){
|
| 972 |
+
const wrap = document.querySelector('.video-wrap');
|
| 973 |
+
const c = document.getElementById('sub-canvas');
|
| 974 |
+
c.width = wrap.clientWidth;
|
| 975 |
+
c.height = wrap.clientHeight;
|
| 976 |
+
const ctx = c.getContext('2d');
|
| 977 |
+
ctx.clearRect(0, 0, c.width, c.height);
|
| 978 |
+
|
| 979 |
+
const sampleText = 'α₯ααα¬ α
α¬αααΊααα―αΈ ααα°αα¬';
|
| 980 |
+
ctx.font = `bold ${fontSize}px "Noto Sans Myanmar", sans-serif`;
|
| 981 |
+
ctx.textAlign = 'center';
|
| 982 |
+
|
| 983 |
+
const tw = ctx.measureText(sampleText).width;
|
| 984 |
+
const pad = 14;
|
| 985 |
+
const bw = tw + pad * 2;
|
| 986 |
+
const bh = fontSize + pad * 1.4;
|
| 987 |
+
|
| 988 |
+
let y;
|
| 989 |
+
if(SUB_POS === 'bottom'){
|
| 990 |
+
y = c.height - margin - bh;
|
| 991 |
+
} else if(SUB_POS === 'top'){
|
| 992 |
+
y = margin;
|
| 993 |
+
} else {
|
| 994 |
+
y = (c.height - bh) / 2;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
const x = c.width / 2;
|
| 998 |
+
|
| 999 |
+
// Dark blur box background
|
| 1000 |
+
ctx.save();
|
| 1001 |
+
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
| 1002 |
+
const r = 10;
|
| 1003 |
+
const bx = x - bw/2;
|
| 1004 |
+
ctx.beginPath();
|
| 1005 |
+
ctx.moveTo(bx+r, y);
|
| 1006 |
+
ctx.lineTo(bx+bw-r, y);
|
| 1007 |
+
ctx.quadraticCurveTo(bx+bw, y, bx+bw, y+r);
|
| 1008 |
+
ctx.lineTo(bx+bw, y+bh-r);
|
| 1009 |
+
ctx.quadraticCurveTo(bx+bw, y+bh, bx+bw-r, y+bh);
|
| 1010 |
+
ctx.lineTo(bx+r, y+bh);
|
| 1011 |
+
ctx.quadraticCurveTo(bx, y+bh, bx, y+bh-r);
|
| 1012 |
+
ctx.lineTo(bx, y+r);
|
| 1013 |
+
ctx.quadraticCurveTo(bx, y, bx+r, y);
|
| 1014 |
+
ctx.closePath();
|
| 1015 |
+
ctx.fill();
|
| 1016 |
+
ctx.restore();
|
| 1017 |
+
|
| 1018 |
+
// Dashed border
|
| 1019 |
+
ctx.save();
|
| 1020 |
+
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
| 1021 |
+
ctx.lineWidth = 1;
|
| 1022 |
+
ctx.setLineDash([4,3]);
|
| 1023 |
+
ctx.strokeRect(x - bw/2, y, bw, bh);
|
| 1024 |
+
ctx.restore();
|
| 1025 |
+
|
| 1026 |
+
// Yellow text
|
| 1027 |
+
ctx.fillStyle = '#FFFF00';
|
| 1028 |
+
ctx.fillText(sampleText, x, y + bh - pad * 0.7);
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
// Redraw canvas on window resize
|
| 1032 |
+
window.addEventListener('resize', ()=>{
|
| 1033 |
+
if(document.getElementById('chk-sub').classList.contains('checked')) updateSubPreview();
|
| 1034 |
+
});
|
| 1035 |
+
|
| 1036 |
</script>
|
| 1037 |
</body>
|
| 1038 |
</html>
|