Phoe2004 commited on
Commit
cd83101
·
verified ·
1 Parent(s): 44609f1

Upload 7 files

Browse files
Files changed (5) hide show
  1. Dockerfile +10 -32
  2. app.py +61 -314
  3. bot.py +1 -1
  4. index.html +24 -382
  5. m_youtube_com_cookies.txt +13 -6
Dockerfile CHANGED
@@ -1,49 +1,27 @@
1
  FROM python:3.11-slim
2
 
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
- fontconfig \
8
- && curl -fsSL https://deno.land/install.sh | sh \
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
- ENV DENO_INSTALL="/root/.deno"
12
- ENV PATH="$DENO_INSTALL/bin:$PATH"
13
-
14
  WORKDIR /app
15
 
16
- # numpy ကို ဦးစွာ install (torch မတိုင်ခင်)
17
- RUN pip install --no-cache-dir "numpy<2"
18
-
19
- # torch CPU only
20
- RUN pip install --no-cache-dir \
21
- torch==2.2.2+cpu torchaudio==2.2.2+cpu \
22
- --index-url https://download.pytorch.org/whl/cpu
23
-
24
  COPY requirements.txt .
25
  RUN pip install --no-cache-dir -r requirements.txt
26
 
27
- # yt-dlp latest
28
- RUN pip install --no-cache-dir -U "yt-dlp[default]"
29
-
30
  COPY app.py .
31
- COPY bot.py .
32
  COPY index.html .
33
- COPY NotoSansMyanmar-Bold.ttf .
34
  COPY m_youtube_com_cookies.txt .
35
- COPY start.sh .
36
- RUN chmod +x start.sh
37
-
38
- # Install NotoSansMyanmar font to system so libass can find it
39
- RUN mkdir -p /usr/local/share/fonts/myanmar \
40
- && cp /app/NotoSansMyanmar-Bold.ttf /usr/local/share/fonts/myanmar/ \
41
- && fc-cache -fv \
42
- && fc-list | grep -i myanmar
43
 
 
44
  RUN mkdir -p outputs
45
 
46
- RUN deno --version && yt-dlp --version && python -c "import numpy; print('numpy', numpy.__version__)"
47
-
48
  EXPOSE 7860
49
- CMD ["./start.sh"]
 
 
1
  FROM python:3.11-slim
2
 
3
+ # Install system dependencies
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ ffmpeg \
6
+ git \
7
+ nodejs \
8
+ npm \
9
  && rm -rf /var/lib/apt/lists/*
10
 
 
 
 
11
  WORKDIR /app
12
 
13
+ # Install Python dependencies
 
 
 
 
 
 
 
14
  COPY requirements.txt .
15
  RUN pip install --no-cache-dir -r requirements.txt
16
 
17
+ # Copy app files
 
 
18
  COPY app.py .
 
19
  COPY index.html .
 
20
  COPY m_youtube_com_cookies.txt .
 
 
 
 
 
 
 
 
21
 
22
+ # Create output directory
23
  RUN mkdir -p outputs
24
 
 
 
25
  EXPOSE 7860
26
+
27
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -84,35 +84,12 @@ BASE_DIR = Path(__file__).parent
84
  COOKIES_FILE = str(BASE_DIR / 'm_youtube_com_cookies.txt')
85
  app = Flask(__name__)
86
 
87
- # #5: YouTube/TikTok/Facebook/Instagram download hard cap 720p
88
  def ytdlp_download(out_tmpl, video_url, timeout=600):
89
- """yt-dlp download — hard cap 720p max, platform-aware, cookies, robust fallback."""
90
- url_lower = video_url.lower()
91
- is_tiktok = 'tiktok.com' in url_lower
92
- is_facebook = 'facebook.com' in url_lower or 'fb.watch' in url_lower
93
- is_instagram = 'instagram.com' in url_lower
94
-
95
- if is_tiktok or is_facebook or is_instagram:
96
- # These platforms don't always have mp4+m4a splits — use best available ≤720p
97
- fmt = (
98
- 'bestvideo[height<=720]+bestaudio'
99
- '/best[height<=720]'
100
- '/best'
101
- )
102
- else:
103
- # YouTube and others — prefer mp4+m4a for clean merge
104
- fmt = (
105
- 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]'
106
- '/bestvideo[height<=720]+bestaudio'
107
- '/best[height<=720][ext=mp4]'
108
- '/best[height<=720]'
109
- '/best[height<=480]'
110
- '/best'
111
- )
112
-
113
  cmd = [
114
  'yt-dlp', '--no-playlist',
115
- '-f', fmt,
116
  '--merge-output-format', 'mp4',
117
  '--no-check-certificates',
118
  ]
@@ -286,69 +263,16 @@ def create_user_fn(uname, coins, caller):
286
  save_db(db); return f"✅ '{uname}' created", uname
287
 
288
  # ── AI ──
289
-
290
- # ── Language-aware system prompts ──
291
- def get_sys_prompt(ct, vo_lang='my'):
292
- """
293
- vo_lang: 'my' = Myanmar (default), 'th' = Thai, 'en' = English
294
- """
295
- if vo_lang == 'th':
296
- # Thai language prompts
297
- if ct == 'Medical/Health':
298
- return (
299
- "คุณคือผู้แปลด้านการแพทย์ภาษาไทย — ภาษาไทยที่พูดในชีวิตประจำวัน\n"
300
- "Rules: 100% ภาษาไทย | ไม่ใช้ภาษาทางการมากเกินไป | เนื้อหาต้นฉบับเท่านั้น\n"
301
- "ใช้ตัวเลขไทย: 1=หนึ่ง, 2=สอง, 10=สิบ, 100=ร้อย, 1000=พัน\n"
302
- "Format EXACTLY:\n[SCRIPT](full thai script here)\n[TITLE](short title)\n[HASHTAGS](exactly 5 hashtags e.g. #สุขภาพ #thailand #health #viral #trending)"
303
- )
304
- else:
305
- return (
306
- "คุณคือนักเขียนสคริปต์สรุปหนังภาษาไทย — เล่าแบบสนุก ภาษาพูดธรรมชาติ\n"
307
- "Rules: 100% ภาษาไทย | ไม่ใช้ภาษาทางการ | เนื้อหาต้นฉบับเท่านั้น\n"
308
- "ใช้ตัวเลขไทย: 1=หนึ่ง, 2=สอง, 10=สิบ, 100=ร้อย, 1000=พัน\n"
309
- "แปลเนื้อหาต่อไปนี้เป็นภาษาไทย (สไตล์เล่าเรื่อง movie recap ที่สนุก)\n"
310
- "ตอบเป็นภาษาไทยเท่านั้น ห้ามมีภาษาอังกฤษในสคริปต์\n"
311
- "Format: [SCRIPT](script)[TITLE](title ≤10 words)[HASHTAGS]#movierecap #thailand"
312
- )
313
- elif vo_lang == 'en':
314
- # English language prompts
315
- if ct == 'Medical/Health':
316
- return (
317
- "You are an English medical content translator — use clear, conversational English.\n"
318
- "Rules: 100% English | conversational tone | original content only\n"
319
- "Write numbers as words: 1=one, 2=two, 10=ten, 100=one hundred\n"
320
- "Format EXACTLY:\n[SCRIPT](full english script here)\n[TITLE](short title)\n[HASHTAGS](exactly 5 hashtags e.g. #health #medical #wellness #viral #trending)"
321
- )
322
- else:
323
- return (
324
- "You are an English movie recap script writer — engaging storytelling tone, conversational.\n"
325
- "Rules: 100% English | conversational not formal | original content only\n"
326
- "Write numbers as words: 1=one, 2=two, 10=ten, 100=one hundred\n"
327
- "Translate and retell the following content in English (movie recap storytelling style)\n"
328
- "Format: [SCRIPT](script)[TITLE](title ≤10 words)[HASHTAGS]#movierecap #english"
329
- )
330
- else:
331
- # Myanmar (default)
332
- if ct == 'Medical/Health':
333
- return (
334
- "မြန်မာ ဆေးဘက် ဘာသာပြန်သူ — spoken Myanmar\n"
335
- "Rules: 100% မြန်မာ | ကျောင်းသုံးစာပေမသုံးရ | ပုဒ်မတိုင်း ။\n"
336
- "ဂဏန်းများကို မြန်မာစကားဖြင့်သာ ရေးပါ — ဥပမာ 1=တစ်, 2=နှစ်, 10=တစ်ဆယ်, 12=တစ်ဆယ့်နှစ်, 20=နှစ်ဆယ်, 100=တစ်ရာ, 1000=တစ်ထောင် — Arabic digit မသုံးရ\n"
337
- "Format EXACTLY:\n[SCRIPT](full myanmar script here)\n[TITLE](short title)\n[HASHTAGS](exactly 5 hashtags e.g. #ကျန်းမာရေး #myanmar #health #viral #trending)"
338
- )
339
- else:
340
- return (
341
- "မြန်မာ movie recap script ရေးသားသူ — spoken Myanmar (နေ့စဉ်ပြောဆိုမှုဘာသာ)\n"
342
- "Rules: 100% မြန်မာဘာသာ | ကျောင်းသုံးစာပေမသုံးရ | မူလcontent သာ | ပုဒ်မတိုင်း ။\n"
343
- "ဂဏန်းများကို မြန်မာစကားဖြင့်သာ ရေးပါ — ဥပမာ 1=တစ်, 2=နှစ်, 10=တစ်ဆယ်, 12=တစ်ဆယ့်နှစ်, 20=နှစ်ဆယ်, 100=တစ်ရာ, 1000=တစ်ထောင် — Arabic digit မသုံးရ\n"
344
- "Translate the following content into Burmese (storytelling tone movie recap tone and keep original content)\n"
345
- "မြန်မာလိုပဲ ဖြေပေးပါ။ အင်္ဂလိပ်လို ဘာမှမပြန်နဲ့။အင်္ဂလိပ်စကားလုံးတွေကိုတွေ့ရင်လည်း မြန်မာလိုပဲ ဘာသာပြန်ပြီး ဖြေပေးပါ)\n"
346
- "Format: [SCRIPT](script)[TITLE](title ≤10 words)[HASHTAGS]#movierecap #မြန်မာ"
347
- )
348
-
349
- # Keep legacy constants for backward compat
350
- SYS_MOVIE = get_sys_prompt('Movie Recap', 'my')
351
- SYS_MED = get_sys_prompt('Medical/Health', 'my')
352
 
353
  NUM_TO_MM_RULE = (
354
  "ဂဏန်းများကို မြန်မာစကားဖြင့်သာ ရေးပါ — "
@@ -356,14 +280,6 @@ NUM_TO_MM_RULE = (
356
  "100=တစ်ရာ, 1000=တစ်ထောင် — Arabic digit မသုံးရ။"
357
  )
358
 
359
- def get_num_rule(vo_lang='my'):
360
- if vo_lang == 'th':
361
- return "ใช้ตัวเลขไทยเท่านั้น: 1=หนึ่ง, 2=สอง, 10=สิบ, 20=ยี่สิบ, 100=ร้อย, 1000=พัน ห้ามใช้ตัวเลขอารบิก"
362
- elif vo_lang == 'en':
363
- return "Write all numbers as English words: 1=one, 2=two, 10=ten, 20=twenty, 100=one hundred, 1000=one thousand — no Arabic digits."
364
- else:
365
- return NUM_TO_MM_RULE
366
-
367
  def call_api(msgs, api='Gemini'):
368
  if api == 'DeepSeek':
369
  keys, base, mdl = DEEPSEEK_KEYS, 'https://api.deepseek.com', 'deepseek-chat'
@@ -402,15 +318,8 @@ def parse_out(text):
402
  ht = ' '.join(tags[:5])
403
  return sc, ti, ht
404
 
405
- def split_txt(txt, vo_lang='my'):
406
- if vo_lang == 'th':
407
- parts = re.split(r'[。\n]', txt)
408
- return [s.strip() for s in parts if s.strip()] or [txt]
409
- elif vo_lang == 'en':
410
- parts = re.split(r'(?<=[.!?])\s+', txt)
411
- return [s.strip() for s in parts if s.strip()] or [txt]
412
- else:
413
- return [s.strip() + '။' for s in re.split(r'[။]', txt) if s.strip()] or [txt]
414
 
415
  def dur(fp):
416
  try:
@@ -441,15 +350,8 @@ def run_tts_sync(sentences, voice_id, rate, tmp_dir):
441
  loop.close()
442
 
443
  def run_edge_preview(voice_id, rate, out_path):
444
- # Choose preview text based on voice language
445
- if voice_id.startswith('th-'):
446
- text = 'สวัสดีครับ ยินดีต้อนรับ'
447
- elif voice_id.startswith('en-'):
448
- text = 'Hello, welcome to Recap Studio.'
449
- else:
450
- text = 'မင်္ဂလာပါ။ ကြိုဆိုပါတယ်။'
451
  async def _run():
452
- await edge_tts.Communicate(text, voice_id, rate=rate).save(out_path)
453
  loop = asyncio.new_event_loop()
454
  try:
455
  loop.run_until_complete(_run())
@@ -662,9 +564,8 @@ def api_draft():
662
  try:
663
  u = (request.form.get('username') or '').strip()
664
  video_url = (request.form.get('video_url') or '').strip()
665
- ct = request.form.get('content_type', 'Movie Recap')
666
- api = request.form.get('ai_model', 'Gemini')
667
- vo_lang = request.form.get('vo_lang', 'my') # 'my', 'th', 'en'
668
  video_file = request.files.get('video_file')
669
 
670
  if not u: return jsonify(ok=False, msg='❌ Not logged in')
@@ -696,19 +597,12 @@ def api_draft():
696
  res = run_stage('whisper', whisper_model.transcribe, vpath, fp16=False)
697
  tr = res['text']; lang = res.get('language', 'en')
698
 
699
- if vo_lang == 'en':
700
- # English skip AI API, return whisper transcript directly
701
- sc = tr.strip()
702
- ti = sc[:60].strip() + ('' if len(sc) > 60 else '')
703
- ht = '#english #movierecap #viral #foryou #trending'
704
- key_n = 'Whisper Direct'
705
- else:
706
- sys_p = get_sys_prompt(ct, vo_lang)
707
- sys_p = sys_p + '\n' + get_num_rule(vo_lang)
708
- out_txt, key_n = run_stage('ai', call_api,
709
- [{'role':'system','content':sys_p},
710
- {'role':'user','content':f'Language:{lang}\n\n{tr}'}], api=api)
711
- sc, ti, ht = parse_out(out_txt)
712
 
713
  rem = -1
714
  if not is_adm: _, rem = deduct(u, 1); upd_stat(u, 'tr')
@@ -737,109 +631,11 @@ def _build_audio_filter(mpath, ad):
737
  else:
738
  return f'[1:a]{voice_chain}[outa]'
739
 
740
- # ── Mid-section Audio Sync Correction ──
741
- def _get_mid_range(duration):
742
- """
743
- Return (start_ratio, end_ratio) for middle section based on total duration.
744
- """
745
- if duration < 180: # < 3 min
746
- return 0.30, 0.70
747
- elif duration < 300: # 3–5 min
748
- return 0.25, 0.75
749
- elif duration < 600: # 5–10 min
750
- return 0.20, 0.80
751
- else: # > 10 min
752
- return 0.15, 0.85
753
-
754
- def _fix_mid_sync(audio_path, video_dur, audio_dur, tmp_dir):
755
- """
756
- Split audio into 3 parts: head / middle / tail.
757
- Apply atempo correction ONLY to middle part if drift > 0.2s.
758
- Recombine and return new audio path (or original if no fix needed).
759
- Pitch is preserved (atempo only, no asetrate).
760
- """
761
- drift = audio_dur - video_dur
762
- if abs(drift) <= 0.2:
763
- print(f'[sync] drift={drift:.3f}s ≤ 0.2s — skip mid-sync')
764
- return audio_path
765
-
766
- s_ratio, e_ratio = _get_mid_range(audio_dur)
767
- t_start = audio_dur * s_ratio
768
- t_end = audio_dur * e_ratio
769
- mid_dur = t_end - t_start
770
-
771
- # Target mid duration after correction
772
- # We want total audio ≈ video_dur
773
- # head + mid_corrected + tail = video_dur
774
- head_dur = t_start
775
- tail_dur = audio_dur - t_end
776
- mid_target = video_dur - head_dur - tail_dur
777
-
778
- if mid_target <= 0:
779
- print(f'[sync] mid_target invalid ({mid_target:.3f}s) — skip')
780
- return audio_path
781
-
782
- tempo = mid_dur / mid_target
783
- # atempo range: 0.5 ~ 2.0 (chain if needed)
784
- tempo = max(0.5, min(2.0, tempo))
785
-
786
- print(f'[sync] drift={drift:.3f}s | mid {t_start:.2f}s~{t_end:.2f}s | tempo={tempo:.4f}x')
787
-
788
- head_f = f'{tmp_dir}/sync_head.mp3'
789
- mid_f = f'{tmp_dir}/sync_mid.mp3'
790
- tail_f = f'{tmp_dir}/sync_tail.mp3'
791
- mid_fx = f'{tmp_dir}/sync_mid_fx.mp3'
792
- out_f = f'{tmp_dir}/sync_fixed.mp3'
793
- lst_f = f'{tmp_dir}/sync_list.txt'
794
-
795
- try:
796
- # Cut head
797
- subprocess.run(
798
- f'ffmpeg -y -i "{audio_path}" -ss 0 -t {t_start:.6f} '
799
- f'-c:a libmp3lame -q:a 2 "{head_f}"',
800
- shell=True, check=True, capture_output=True)
801
-
802
- # Cut middle
803
- subprocess.run(
804
- f'ffmpeg -y -i "{audio_path}" -ss {t_start:.6f} -t {mid_dur:.6f} '
805
- f'-c:a libmp3lame -q:a 2 "{mid_f}"',
806
- shell=True, check=True, capture_output=True)
807
-
808
- # Cut tail
809
- subprocess.run(
810
- f'ffmpeg -y -i "{audio_path}" -ss {t_end:.6f} '
811
- f'-c:a libmp3lame -q:a 2 "{tail_f}"',
812
- shell=True, check=True, capture_output=True)
813
-
814
- # Apply atempo to middle (pitch unchanged)
815
- subprocess.run(
816
- f'ffmpeg -y -i "{mid_f}" -af "atempo={tempo:.6f}" '
817
- f'-c:a libmp3lame -q:a 2 "{mid_fx}"',
818
- shell=True, check=True, capture_output=True)
819
-
820
- # Concat head + mid_fixed + tail
821
- with open(lst_f, 'w') as lf:
822
- for f in [head_f, mid_fx, tail_f]:
823
- if os.path.exists(f) and os.path.getsize(f) > 0:
824
- lf.write(f"file '{os.path.abspath(f)}'\n")
825
- subprocess.run(
826
- f'ffmpeg -y -f concat -safe 0 -i "{lst_f}" '
827
- f'-c:a libmp3lame -q:a 2 "{out_f}"',
828
- shell=True, check=True, capture_output=True)
829
-
830
- print(f'[sync] mid-sync done → {out_f}')
831
- return out_f
832
-
833
- except Exception as e:
834
- print(f'[sync] mid-sync failed: {e} — using original audio')
835
- return audio_path
836
-
837
  # ── #6: Video render — smaller output file ──
838
- def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
839
- logo_path=None, logo_x=10, logo_y=10, logo_w=80):
840
  raw_ratio = ad / vd
841
 
842
- # ── Step 1: Pre-process video — resize + fix even dims ──
843
  pre_out = vpath + '_pre.mp4'
844
  pre_cmd = (
845
  f'ffmpeg -y -hide_banner -loglevel error '
@@ -850,7 +646,7 @@ def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
850
  )
851
  subprocess.run(pre_cmd, shell=True, check=True)
852
 
853
- # ── Step 2: Precise sync calculation ──
854
  sync_ratio = max(0.5, min(3.0, raw_ratio))
855
  need_loop = raw_ratio > 3.0
856
  need_trim = raw_ratio < 0.5
@@ -866,74 +662,50 @@ def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
866
  base.append(f'setpts={sync_ratio:.6f}*PTS')
867
  if flip: base.append('hflip')
868
  if col: base.append('eq=brightness=0.06:contrast=1.2:saturation=1.4')
869
- base.append('scale=iw:ih')
 
 
870
  base.append('format=yuv420p')
871
  base_str = ','.join(base)
872
 
873
  if crop == '9:16':
874
- vbase = (
875
  f'[0:v]{base_str},split[s1][s2];'
876
  f'[s1]scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280,boxblur=20:20[bg];'
877
  f'[s2]scale=720:1280:force_original_aspect_ratio=decrease[fg];'
878
  f'[bg][fg]overlay=(W-w)/2:(H-h)/2'
879
  )
880
  elif crop == '16:9':
881
- vbase = (
882
  f'[0:v]{base_str},split[s1][s2];'
883
  f'[s1]scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,boxblur=20:20[bg];'
884
  f'[s2]scale=1280:720:force_original_aspect_ratio=decrease[fg];'
885
  f'[bg][fg]overlay=(W-w)/2:(H-h)/2'
886
  )
887
  elif crop == '1:1':
888
- vbase = (
889
  f'[0:v]{base_str},split[s1][s2];'
890
  f'[s1]scale=720:720:force_original_aspect_ratio=increase,crop=720:720,boxblur=20:20[bg];'
891
  f'[s2]scale=720:720:force_original_aspect_ratio=decrease[fg];'
892
  f'[bg][fg]overlay=(W-w)/2:(H-h)/2'
893
  )
894
  else:
895
- vbase = f'[0:v]{base_str}'
896
-
897
- # ── Logo + Watermark — build as single continuous chain, no label re-use ──
898
- logo_idx = None
899
- if logo_path and os.path.exists(logo_path):
900
- logo_idx = 2 if not mpath else 3
901
 
902
- if wmk and logo_idx is not None:
903
  cn = wmk.replace("'","").replace("\\","").replace(":","")
904
- vff = (
905
- f'{vbase},'
906
- f'drawtext=text=\'{cn}\':x=w-tw-40:y=40:fontsize=35:fontcolor=white:shadowcolor=black:shadowx=2:shadowy=2'
907
- f'[vwmk];'
908
- f'[{logo_idx}:v]scale={logo_w}:-1[logo];'
909
- f'[vwmk][logo]overlay={logo_x}:{logo_y}[outv]'
910
- )
911
- elif wmk:
912
- cn = wmk.replace("'","").replace("\\","").replace(":","")
913
- vff = (
914
- f'{vbase},'
915
- f'drawtext=text=\'{cn}\':x=w-tw-40:y=40:fontsize=35:fontcolor=white:shadowcolor=black:shadowx=2:shadowy=2'
916
- f'[outv]'
917
- )
918
- elif logo_idx is not None:
919
- vff = (
920
- f'{vbase}[vbase];'
921
- f'[{logo_idx}:v]scale={logo_w}:-1[logo];'
922
- f'[vbase][logo]overlay={logo_x}:{logo_y}[outv]'
923
- )
924
  else:
925
- vff = f'{vbase}[outv]'
926
 
927
  af = _build_audio_filter(mpath, ad)
928
 
929
  inp = f'-fflags +genpts+igndts -err_detect ignore_err -i "{pre_out}" -i "{cmb}"'
930
  if mpath:
931
  inp += f' -stream_loop -1 -i "{mpath}"'
932
- if logo_idx is not None:
933
- inp += f' -i "{logo_path}"'
934
 
935
  cmd = (
936
- f'nice -n 10 ffmpeg -y -hide_banner -loglevel error {inp} '
937
  f'-filter_complex "{vff};{af}" '
938
  f'-map "[outv]" -map "[outa]" '
939
  f'-c:v libx264 -crf 26 -preset medium -pix_fmt yuv420p '
@@ -961,11 +733,11 @@ def api_process():
961
  crop = request.form.get('crop', '9:16')
962
  flip = request.form.get('flip', '0') == '1'
963
  col = request.form.get('color', '0') == '1'
964
- vo_lang = request.form.get('vo_lang', 'my')
965
- # Speed default per language (can be overridden by slider)
966
- LANG_SPD = {'th': 20, 'en': 0, 'my': 30}
967
- if request.form.get('speed') is None:
968
- spd = LANG_SPD.get(vo_lang, 30)
969
  is_adm = (u == ADMIN_U)
970
  if not is_adm and get_coins(u) < 1:
971
  return jsonify(ok=False, msg='❌ Not enough coins')
@@ -993,15 +765,8 @@ def api_process():
993
  mpath = f'{tmp_dir}/music.mp3'
994
  music_file.save(mpath)
995
 
996
- logo_path = None
997
- logo_file = request.files.get('logo_file')
998
- logo_x = int(request.form.get('logo_x', 10))
999
- logo_y = int(request.form.get('logo_y', 10))
1000
- logo_w = int(request.form.get('logo_w', 80))
1001
- if logo_file and logo_file.filename:
1002
- ext = Path(logo_file.filename).suffix or '.png'
1003
- logo_path = f'{tmp_dir}/logo{ext}'
1004
- logo_file.save(logo_path)
1005
  if engine == 'gemini':
1006
  parts = run_stage('tts', run_gemini_tts_sync, sentences, voice_id, tmp_dir, speed=spd)
1007
  else:
@@ -1020,8 +785,10 @@ def api_process():
1020
  if vd <= 0: raise Exception('Video duration read failed')
1021
  if ad <= 0: raise Exception('Audio duration read failed')
1022
 
1023
- _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
1024
- logo_path=logo_path, logo_x=logo_x, logo_y=logo_y, logo_w=logo_w)
 
 
1025
  return jsonify(ok=True, output_url=f'/outputs/final_{tid}.mp4', coins=rem)
1026
 
1027
  finally:
@@ -1037,7 +804,7 @@ def api_process():
1037
  def api_progress(tid):
1038
  def generate():
1039
  sent_done = False
1040
- for _ in range(1800): # 1800 × 0.4s = 12 minutes max
1041
  p = job_progress.get(tid)
1042
  if p is None:
1043
  yield f"data: {json.dumps({'pct':0,'msg':'Please wait…'})}\n\n"
@@ -1048,7 +815,7 @@ def api_progress(tid):
1048
  break
1049
  time.sleep(0.4)
1050
  if not sent_done:
1051
- yield f"data: {json.dumps({'pct':0,'msg':'Timeout — process took too long','error':True})}\n\n"
1052
  return Response(generate(), mimetype='text/event-stream',
1053
  headers={'Cache-Control':'no-cache','X-Accel-Buffering':'no'})
1054
 
@@ -1067,17 +834,8 @@ def api_process_all():
1067
  col = request.form.get('color', '0') == '1'
1068
  ct = request.form.get('content_type', 'Movie Recap')
1069
  api = request.form.get('ai_model', 'Gemini')
1070
- vo_lang = request.form.get('vo_lang', 'my') # 'my', 'th', 'en'
1071
- # Speed default per language (can be overridden by slider)
1072
- LANG_SPD = {'th': 20, 'en': 0, 'my': 30}
1073
- if request.form.get('speed') is None:
1074
- spd = LANG_SPD.get(vo_lang, 30)
1075
  video_file = request.files.get('video_file')
1076
  music_file = request.files.get('music_file')
1077
- logo_file = request.files.get('logo_file')
1078
- logo_x = int(request.form.get('logo_x', 10))
1079
- logo_y = int(request.form.get('logo_y', 10))
1080
- logo_w = int(request.form.get('logo_w', 80))
1081
  client_tid = (request.form.get('tid') or '').strip()
1082
 
1083
  if not u: return jsonify(ok=False, msg='❌ Not logged in')
@@ -1116,31 +874,21 @@ def api_process_all():
1116
  res = run_stage('whisper', whisper_model.transcribe, vpath, fp16=False)
1117
  tr = res['text']; src_lang = res.get('language', 'en')
1118
 
1119
- if vo_lang == 'en':
1120
- # English skip AI API, use whisper transcript directly
1121
- sc = tr.strip()
1122
- caption_text = sc[:60].strip() + ('' if len(sc) > 60 else '')
1123
- hashtags = '#english #movierecap #viral #foryou #trending'
1124
- else:
1125
- job_progress[tid] = {'pct': 45, 'msg': '🤖 Generating AI script…', 'done': False}
1126
- sys_p = get_sys_prompt(ct, vo_lang)
1127
- sys_p = sys_p + '\n' + get_num_rule(vo_lang)
1128
- out_txt, _ = run_stage('ai', call_api,
1129
- [{'role':'system','content':sys_p},
1130
- {'role':'user','content':f'Language:{src_lang}\n\n{tr}'}], api=api)
1131
- sc, caption_text, hashtags = parse_out(out_txt)
1132
 
1133
  if music_file and music_file.filename:
1134
  mpath = f'{tmp_dir}/music.mp3'
1135
  music_file.save(mpath)
1136
 
1137
- logo_path = None
1138
- if logo_file and logo_file.filename:
1139
- ext = Path(logo_file.filename).suffix or '.png'
1140
- logo_path = f'{tmp_dir}/logo{ext}'
1141
- logo_file.save(logo_path)
1142
  rate = f'+{spd}%'
1143
- sentences = split_txt(sc, vo_lang)
1144
  if engine == 'gemini':
1145
  parts = run_stage('tts', run_gemini_tts_sync, sentences, voice_id, tmp_dir, speed=spd)
1146
  else:
@@ -1161,8 +909,7 @@ def api_process_all():
1161
  if vd <= 0: raise Exception('Video duration read failed')
1162
  if ad <= 0: raise Exception('Audio duration read failed')
1163
 
1164
- _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
1165
- logo_path=logo_path, logo_x=logo_x, logo_y=logo_y, logo_w=logo_w)
1166
 
1167
  rem = -1
1168
  if not is_adm:
 
84
  COOKIES_FILE = str(BASE_DIR / 'm_youtube_com_cookies.txt')
85
  app = Flask(__name__)
86
 
87
+ # #5: YouTube download default max 720p (never exceeds 720p)
88
  def ytdlp_download(out_tmpl, video_url, timeout=600):
89
+ """yt-dlp download — hard cap 720p max, cookies, robust format fallback."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  cmd = [
91
  'yt-dlp', '--no-playlist',
92
+ '-f', 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=720]+bestaudio/best[height<=720][ext=mp4]/best[height<=720]/best[height<=480]/best',
93
  '--merge-output-format', 'mp4',
94
  '--no-check-certificates',
95
  ]
 
263
  save_db(db); return f"✅ '{uname}' created", uname
264
 
265
  # ── AI ──
266
+ SYS_MOVIE = ("မြန်မာ movie recap script ရေးသားသူ — spoken Myanmar (နေ့စဉ်ပြောဆိုမှုဘာသာ)\n"
267
+ "Rules: 100% မြန်မာဘာသာ | ကျောင်းသုံးစာပေမသုံးရ | မူလcontent သာ | ပုဒ်မတိုင်း ။\n"
268
+ "ဂဏန်းများကို မြန်မာစကားဖြင့်သာ ရေးပါ — ဥပမာ 1=တစ်, 2=နှစ်, 10=တစ်ဆယ်, 12=တစ်ဆယ့်နှစ်, 20=နှစ်ဆယ်, 100=တစ်ရာ, 1000=တစ်ထောင် — Arabic digit မသုံးရ\n"
269
+ "Translate the following content into Burmese (storytelling tone movie recap tone and keep original content)\n"
270
+ "မြန်မာလိုပဲ ဖြေပေးပါ။ အင်္ဂလိပ်လို ဘာမှမပြန်နဲ့။အင်္ဂလိပ်စကားလုံးတွေကိုတွေ့ရင်လည်း မြန်မာလိုပဲ ဘာသာပြန်ပြီး ဖြေပေးပါ)\n"
271
+ "Format: [SCRIPT](script)[TITLE](title ≤10 words)[HASHTAGS]#movierecap #မြန်မာ")
272
+ SYS_MED = ("မြန်မာ ဆေးဘက် ဘာသာပြန်သူ — spoken Myanmar\n"
273
+ "Rules: 100% မြန်မာ | ကျောင်းသုံးစာပေမသုံးရ | ပုဒ်မတိုင်း ။\n"
274
+ "ဂဏန်းများကို မြန်မာစကားဖြင့်သာ ရေးပါ — ဥပမာ 1=တစ်, 2=နှစ်, 10=တစ်ဆယ်, 12=တစ်ဆယ့်နှစ်, 20=နှစ်ဆယ်, 100=တစ်ရာ, 1000=တစ်ထောင် — Arabic digit မသုံးရ\n"
275
+ "Format EXACTLY:\n[SCRIPT](full myanmar script here)\n[TITLE](short title)\n[HASHTAGS](exactly 5 hashtags e.g. #ကျန်းမာရေး #myanmar #health #viral #trending)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
  NUM_TO_MM_RULE = (
278
  "ဂဏန်းများကို မြန်မာစကားဖြင့်သာ ရေးပါ — "
 
280
  "100=တစ်ရာ, 1000=တစ်ထောင် — Arabic digit မသုံးရ။"
281
  )
282
 
 
 
 
 
 
 
 
 
283
  def call_api(msgs, api='Gemini'):
284
  if api == 'DeepSeek':
285
  keys, base, mdl = DEEPSEEK_KEYS, 'https://api.deepseek.com', 'deepseek-chat'
 
318
  ht = ' '.join(tags[:5])
319
  return sc, ti, ht
320
 
321
+ def split_txt(txt):
322
+ return [s.strip() + '' for s in re.split(r'[။]', txt) if s.strip()] or [txt]
 
 
 
 
 
 
 
323
 
324
  def dur(fp):
325
  try:
 
350
  loop.close()
351
 
352
  def run_edge_preview(voice_id, rate, out_path):
 
 
 
 
 
 
 
353
  async def _run():
354
+ await edge_tts.Communicate('မင်္ဂလာပါ။', voice_id, rate=rate).save(out_path)
355
  loop = asyncio.new_event_loop()
356
  try:
357
  loop.run_until_complete(_run())
 
564
  try:
565
  u = (request.form.get('username') or '').strip()
566
  video_url = (request.form.get('video_url') or '').strip()
567
+ ct = request.form.get('content_type', 'Movie Recap')
568
+ api = request.form.get('ai_model', 'Gemini')
 
569
  video_file = request.files.get('video_file')
570
 
571
  if not u: return jsonify(ok=False, msg='❌ Not logged in')
 
597
  res = run_stage('whisper', whisper_model.transcribe, vpath, fp16=False)
598
  tr = res['text']; lang = res.get('language', 'en')
599
 
600
+ sys_p = SYS_MED if ct == 'Medical/Health' else SYS_MOVIE
601
+ sys_p = sys_p + '\n' + NUM_TO_MM_RULE
602
+ out_txt, key_n = run_stage('ai', call_api,
603
+ [{'role':'system','content':sys_p},
604
+ {'role':'user','content':f'Language:{lang}\n\n{tr}'}], api=api)
605
+ sc, ti, ht = parse_out(out_txt)
 
 
 
 
 
 
 
606
 
607
  rem = -1
608
  if not is_adm: _, rem = deduct(u, 1); upd_stat(u, 'tr')
 
631
  else:
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) ──
639
  pre_out = vpath + '_pre.mp4'
640
  pre_cmd = (
641
  f'ffmpeg -y -hide_banner -loglevel error '
 
646
  )
647
  subprocess.run(pre_cmd, shell=True, check=True)
648
 
649
+ # ── Step 2: Precise sync calculation (recapfull logic) ──
650
  sync_ratio = max(0.5, min(3.0, raw_ratio))
651
  need_loop = raw_ratio > 3.0
652
  need_trim = raw_ratio < 0.5
 
662
  base.append(f'setpts={sync_ratio:.6f}*PTS')
663
  if flip: base.append('hflip')
664
  if col: base.append('eq=brightness=0.06:contrast=1.2:saturation=1.4')
665
+
666
+ # Safe max-720p scale — no min() needed since pre-step already fixed dims
667
+ base.append('scale=iw:ih') # dims already fixed by pre-step
668
  base.append('format=yuv420p')
669
  base_str = ','.join(base)
670
 
671
  if crop == '9:16':
672
+ v_layout = (
673
  f'[0:v]{base_str},split[s1][s2];'
674
  f'[s1]scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280,boxblur=20:20[bg];'
675
  f'[s2]scale=720:1280:force_original_aspect_ratio=decrease[fg];'
676
  f'[bg][fg]overlay=(W-w)/2:(H-h)/2'
677
  )
678
  elif crop == '16:9':
679
+ v_layout = (
680
  f'[0:v]{base_str},split[s1][s2];'
681
  f'[s1]scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,boxblur=20:20[bg];'
682
  f'[s2]scale=1280:720:force_original_aspect_ratio=decrease[fg];'
683
  f'[bg][fg]overlay=(W-w)/2:(H-h)/2'
684
  )
685
  elif crop == '1:1':
686
+ v_layout = (
687
  f'[0:v]{base_str},split[s1][s2];'
688
  f'[s1]scale=720:720:force_original_aspect_ratio=increase,crop=720:720,boxblur=20:20[bg];'
689
  f'[s2]scale=720:720:force_original_aspect_ratio=decrease[fg];'
690
  f'[bg][fg]overlay=(W-w)/2:(H-h)/2'
691
  )
692
  else:
693
+ v_layout = f'[0:v]{base_str}'
 
 
 
 
 
694
 
695
+ if wmk:
696
  cn = wmk.replace("'","").replace("\\","").replace(":","")
697
+ vff = f"{v_layout},drawtext=text='{cn}':x=w-tw-40:y=40:fontsize=35:fontcolor=white:shadowcolor=black:shadowx=2:shadowy=2[outv]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  else:
699
+ vff = f'{v_layout}[outv]'
700
 
701
  af = _build_audio_filter(mpath, ad)
702
 
703
  inp = f'-fflags +genpts+igndts -err_detect ignore_err -i "{pre_out}" -i "{cmb}"'
704
  if mpath:
705
  inp += f' -stream_loop -1 -i "{mpath}"'
 
 
706
 
707
  cmd = (
708
+ f'nice -n 19 ffmpeg -y -hide_banner -loglevel error {inp} '
709
  f'-filter_complex "{vff};{af}" '
710
  f'-map "[outv]" -map "[outa]" '
711
  f'-c:v libx264 -crf 26 -preset medium -pix_fmt yuv420p '
 
733
  crop = request.form.get('crop', '9:16')
734
  flip = request.form.get('flip', '0') == '1'
735
  col = request.form.get('color', '0') == '1'
736
+ video_file = request.files.get('video_file')
737
+ music_file = request.files.get('music_file')
738
+
739
+ if not u: return jsonify(ok=False, msg=' Not logged in')
740
+ if not sc: return jsonify(ok=False, msg='❌ No script')
741
  is_adm = (u == ADMIN_U)
742
  if not is_adm and get_coins(u) < 1:
743
  return jsonify(ok=False, msg='❌ Not enough coins')
 
765
  mpath = f'{tmp_dir}/music.mp3'
766
  music_file.save(mpath)
767
 
768
+ rate = f'+{spd}%'
769
+ sentences = split_txt(sc)
 
 
 
 
 
 
 
770
  if engine == 'gemini':
771
  parts = run_stage('tts', run_gemini_tts_sync, sentences, voice_id, tmp_dir, speed=spd)
772
  else:
 
785
  if vd <= 0: raise Exception('Video duration read failed')
786
  if ad <= 0: raise Exception('Audio duration read failed')
787
 
788
+ _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file)
789
+
790
+ rem = -1
791
+ if not is_adm: _, rem = deduct(u, 1); upd_stat(u, 'vd')
792
  return jsonify(ok=True, output_url=f'/outputs/final_{tid}.mp4', coins=rem)
793
 
794
  finally:
 
804
  def api_progress(tid):
805
  def generate():
806
  sent_done = False
807
+ for _ in range(600):
808
  p = job_progress.get(tid)
809
  if p is None:
810
  yield f"data: {json.dumps({'pct':0,'msg':'Please wait…'})}\n\n"
 
815
  break
816
  time.sleep(0.4)
817
  if not sent_done:
818
+ yield f"data: {json.dumps({'pct':0,'msg':'Timeout','error':True})}\n\n"
819
  return Response(generate(), mimetype='text/event-stream',
820
  headers={'Cache-Control':'no-cache','X-Accel-Buffering':'no'})
821
 
 
834
  col = request.form.get('color', '0') == '1'
835
  ct = request.form.get('content_type', 'Movie Recap')
836
  api = request.form.get('ai_model', 'Gemini')
 
 
 
 
 
837
  video_file = request.files.get('video_file')
838
  music_file = request.files.get('music_file')
 
 
 
 
839
  client_tid = (request.form.get('tid') or '').strip()
840
 
841
  if not u: return jsonify(ok=False, msg='❌ Not logged in')
 
874
  res = run_stage('whisper', whisper_model.transcribe, vpath, fp16=False)
875
  tr = res['text']; src_lang = res.get('language', 'en')
876
 
877
+ job_progress[tid] = {'pct': 45, 'msg': '🤖 Generating AI script…', 'done': False}
878
+ sys_p = SYS_MED if ct == 'Medical/Health' else SYS_MOVIE
879
+ sys_p = sys_p + '\n' + NUM_TO_MM_RULE
880
+ out_txt, _ = run_stage('ai', call_api,
881
+ [{'role':'system','content':sys_p},
882
+ {'role':'user','content':f'Language:{src_lang}\n\n{tr}'}], api=api)
883
+ sc, caption_text, hashtags = parse_out(out_txt)
 
 
 
 
 
 
884
 
885
  if music_file and music_file.filename:
886
  mpath = f'{tmp_dir}/music.mp3'
887
  music_file.save(mpath)
888
 
889
+ job_progress[tid] = {'pct': 62, 'msg': '🔊 Generating TTS audio…', 'done': False}
 
 
 
 
890
  rate = f'+{spd}%'
891
+ sentences = split_txt(sc)
892
  if engine == 'gemini':
893
  parts = run_stage('tts', run_gemini_tts_sync, sentences, voice_id, tmp_dir, speed=spd)
894
  else:
 
909
  if vd <= 0: raise Exception('Video duration read failed')
910
  if ad <= 0: raise Exception('Audio duration read failed')
911
 
912
+ _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file)
 
913
 
914
  rem = -1
915
  if not is_adm:
bot.py CHANGED
@@ -481,7 +481,7 @@ async def _do_process(bot, cid, prog_msg_id, pending_url=None, pending_file_id=N
481
  with open(out_file, 'rb') as vf:
482
  await bot.send_video(chat_id=cid, video=vf,
483
  caption=f"🎬 *{caption_text}*\n\n{hashtags}\n\n🪙 ကျန် Coins: *{fmt_coins(s['coins'])}*",
484
- parse_mode=ParseMode.MARKDOWN, supports_streaming=True, read_timeout=600, write_timeout=600)
485
  await bot.delete_message(chat_id=cid, message_id=prog_msg_id)
486
  except Exception as e:
487
  logger.exception(f'[{tid}] Error: {e}')
 
481
  with open(out_file, 'rb') as vf:
482
  await bot.send_video(chat_id=cid, video=vf,
483
  caption=f"🎬 *{caption_text}*\n\n{hashtags}\n\n🪙 ကျန် Coins: *{fmt_coins(s['coins'])}*",
484
+ parse_mode=ParseMode.MARKDOWN, supports_streaming=True, read_timeout=300, write_timeout=300)
485
  await bot.delete_message(chat_id=cid, message_id=prog_msg_id)
486
  except Exception as e:
487
  logger.exception(f'[{tid}] Error: {e}')
index.html CHANGED
@@ -95,11 +95,8 @@ body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:1
95
  .vcard:hover{border-color:var(--border2);background:var(--bg2)}
96
  .vcard.selected{border-color:var(--amber);background:rgba(245,166,35,.06)}
97
  .vcard-name{font-size:.72rem;font-weight:600;margin-bottom:2px;line-height:1.2}
98
- .vcard-sub{font-size:.6rem;color:var(--muted);margin-bottom:4px}
99
- .vcard-play{display:flex;align-items:center;justify-content:center;gap:3px;margin-top:4px;padding:3px 0;border-radius:4px;background:rgba(245,166,35,.08);border:1px solid rgba(245,166,35,.15);color:var(--amber2);font-size:.6rem;font-weight:600;cursor:pointer;transition:.2s}
100
- .vcard-play:hover{background:rgba(245,166,35,.18)}
101
- .vcard-play.playing{background:rgba(0,184,148,.08);border-color:rgba(0,184,148,.2);color:var(--green)}
102
- .vcard-play.gemini-hide{display:none !important}
103
 
104
  /* ── SPEED (hidden until toggle) ── */
105
  .speed-toggle{display:flex;align-items:center;gap:6px;margin-top:10px;cursor:pointer;font-size:.78rem;color:var(--muted2);font-weight:500;user-select:none}
@@ -151,7 +148,7 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
151
  .preview-top{padding:12px 14px;border-bottom:1px solid var(--border);font-size:.68rem;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:6px}
152
  .preview-top i{color:var(--amber)}
153
  .video-wrap{aspect-ratio:9/16;background:#f0f1f4;position:relative;max-height:400px;overflow:hidden}
154
- .video-wrap video{width:100%;height:100%;object-fit:contain;position:relative;z-index:2}
155
  .video-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--muted)}
156
  .video-placeholder i{font-size:2.5rem}
157
  .video-placeholder p{font-size:.8rem}
@@ -200,16 +197,6 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
200
  .spinning{animation:spin .8s linear infinite}
201
  @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
202
  .fade-in{animation:fadeIn .3s ease forwards}
203
-
204
- /* ── LANGUAGE SELECTOR ── */
205
- .lang-btn{background:var(--bg3);border:1.5px solid var(--border);border-radius:8px;padding:8px 4px;text-align:center;cursor:pointer;transition:.2s;font-size:.75rem;font-weight:600;color:var(--muted2);user-select:none;line-height:1.6}
206
- .lang-btn:hover{border-color:var(--border2);color:var(--text)}
207
- .lang-btn.active{border-color:var(--amber);background:rgba(245,166,35,.08);color:var(--amber2)}
208
-
209
- /* ── LOGO POSITION GRID ── */
210
- .pos-btn{background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:6px 0;font-size:.9rem;cursor:pointer;transition:.2s;color:var(--muted2);font-family:var(--sans)}
211
- .pos-btn:hover{border-color:var(--amber);color:var(--amber);background:rgba(245,166,35,.06)}
212
- .pos-btn.active{border-color:var(--amber);background:rgba(245,166,35,.1);color:var(--amber2)}
213
  </style>
214
  </head>
215
  <body>
@@ -339,23 +326,6 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
339
  <!-- SETTINGS -->
340
  <div class="card">
341
  <div class="card-label"><i class="fas fa-cog"></i> SETTINGS</div>
342
-
343
- <!-- LANGUAGE SELECTOR -->
344
- <div style="margin-bottom:12px">
345
- <div class="field-label"><i class="fas fa-globe"></i> Output Language</div>
346
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
347
- <div class="lang-btn active" id="lang-my" onclick="switchLang('my')">
348
- <span style="font-size:1.1rem">🇲🇲</span><br><span>Myanmar</span>
349
- </div>
350
- <div class="lang-btn" id="lang-th" onclick="switchLang('th')">
351
- <span style="font-size:1.1rem">🇹🇭</span><br><span>Thailand</span>
352
- </div>
353
- <div class="lang-btn" id="lang-en" onclick="switchLang('en')">
354
- <span style="font-size:1.1rem">🇬🇧</span><br><span>English</span>
355
- </div>
356
- </div>
357
- </div>
358
-
359
  <div class="grid2">
360
  <div>
361
  <div class="field-label"><i class="fas fa-crop-alt"></i> Crop Ratio</div>
@@ -396,62 +366,6 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
396
  <span style="font-size:.8rem;color:var(--muted2)"><i class="fas fa-music"></i> <span id="music-name">Choose MP3</span></span>
397
  </div>
398
  </div>
399
-
400
- <!-- LOGO -->
401
- <div style="margin-top:8px">
402
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
403
- <div class="field-label" style="margin:0"><i class="fas fa-image"></i> Logo Overlay</div>
404
- <div class="check-item" id="chk-logo" onclick="toggleLogo(this)" style="padding:4px 12px;border-radius:20px;font-size:.75rem;width:auto;gap:6px">
405
- <div class="check-box"></div><span id="logo-lbl">Off</span>
406
- </div>
407
- </div>
408
- <div id="logo-section" style="display:none">
409
- <div class="upload-area" style="padding:12px" onclick="document.getElementById('logo-file').click()">
410
- <input type="file" id="logo-file" accept="image/*" style="display:none" onchange="onLogoSelect(this)">
411
- <span style="font-size:.8rem;color:var(--muted2)"><i class="fas fa-image"></i> <span id="logo-name">Choose Logo (PNG/JPG)</span></span>
412
- </div>
413
- <!-- Logo position picker -->
414
- <div id="logo-pos-wrap" style="display:none;margin-top:8px">
415
- <div class="field-label" style="margin-bottom:6px"><i class="fas fa-crosshairs"></i> Logo Position</div>
416
- <div style="display:flex;gap:10px;align-items:flex-start">
417
- <!-- Mini preview -->
418
- <div id="logo-preview-canvas" style="position:relative;flex-shrink:0;width:90px;aspect-ratio:9/16;background:#1a1d28;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
419
- <video id="logo-bg-video" style="width:100%;height:100%;object-fit:cover;opacity:0.5;pointer-events:none"></video>
420
- <img id="logo-drag-img" style="position:absolute;pointer-events:none;filter:drop-shadow(0 1px 3px rgba(0,0,0,.6))" draggable="false">
421
- </div>
422
- <!-- Controls -->
423
- <div style="flex:1">
424
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin-bottom:8px">
425
- <button class="pos-btn" onclick="setLogoPos('tl')" title="Top Left">↖</button>
426
- <button class="pos-btn" onclick="setLogoPos('tc')" title="Top Center">↑</button>
427
- <button class="pos-btn" onclick="setLogoPos('tr')" title="Top Right">↗</button>
428
- <button class="pos-btn" onclick="setLogoPos('ml')" title="Mid Left">←</button>
429
- <button class="pos-btn" onclick="setLogoPos('mc')" title="Center">·</button>
430
- <button class="pos-btn" onclick="setLogoPos('mr')" title="Mid Right">→</button>
431
- <button class="pos-btn" onclick="setLogoPos('bl')" title="Bottom Left">↙</button>
432
- <button class="pos-btn" onclick="setLogoPos('bc')" title="Bottom Center">↓</button>
433
- <button class="pos-btn" onclick="setLogoPos('br')" title="Bottom Right">↘</button>
434
- </div>
435
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
436
- <span style="font-size:.7rem;color:var(--muted);white-space:nowrap">Size</span>
437
- <input type="range" id="logo-size" min="40" max="300" value="80" style="flex:1;accent-color:var(--amber)" oninput="onLogoSizeChange(this.value)">
438
- <span id="logo-size-val" style="font-size:.72rem;color:var(--amber2);min-width:32px">80px</span>
439
- </div>
440
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
441
- <span style="font-size:.7rem;color:var(--muted);white-space:nowrap;width:10px">X</span>
442
- <input type="range" id="logo-x-slider" min="0" max="660" value="10" style="flex:1;accent-color:var(--amber)" oninput="onLogoPosSlider()">
443
- <span id="logo-x-val" style="font-size:.72rem;color:var(--muted2);min-width:32px">10</span>
444
- </div>
445
- <div style="display:flex;align-items:center;gap:6px">
446
- <span style="font-size:.7rem;color:var(--muted);white-space:nowrap;width:10px">Y</span>
447
- <input type="range" id="logo-y-slider" min="0" max="1200" value="10" style="flex:1;accent-color:var(--amber)" oninput="onLogoPosSlider()">
448
- <span id="logo-y-val" style="font-size:.72rem;color:var(--muted2);min-width:32px">10</span>
449
- </div>
450
- </div>
451
- </div>
452
- </div>
453
- </div>
454
- </div>
455
  </div>
456
 
457
  <div id="action-full">
@@ -474,9 +388,8 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
474
  <div class="preview-panel">
475
  <div class="preview-box">
476
  <div class="preview-top"><i class="fas fa-play-circle"></i> PREVIEW</div>
477
- <div class="video-wrap" id="video-wrap-box">
478
- <img id="thumb-preview" style="display:none;width:100%;height:100%;object-fit:cover;position:absolute;top:0;left:0;z-index:1" alt="thumbnail">
479
- <video id="preview-video" controls style="display:none;position:relative;z-index:2"></video>
480
  <div class="video-placeholder" id="video-placeholder"><i class="fas fa-film"></i><p>Preview will appear here</p></div>
481
  </div>
482
  <div class="preview-bottom">
@@ -484,7 +397,6 @@ select.input{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xml
484
  <div class="meta-tags" id="meta-tags" style="display:none"></div>
485
  <button class="download-btn" id="download-btn" onclick="downloadVideo()"><i class="fas fa-download"></i> Download MP4</button>
486
  <button class="copy-caption-btn" id="copy-caption-btn" onclick="copyCaption()"><i class="fas fa-copy"></i> Copy Caption</button>
487
- <button class="copy-caption-btn" id="copy-link-btn" onclick="copyVideoLink()" style="display:none;margin-top:6px"><i class="fas fa-link"></i> Copy Video Link</button>
488
  <button class="download-btn" id="download-btn2" onclick="downloadVideo()" style="display:none;margin-top:6px;background:rgba(9,132,227,.06);border-color:rgba(9,132,227,.2);color:var(--cyan)"><i class="fas fa-link"></i> Open Link</button>
489
  </div>
490
  </div>
@@ -562,7 +474,6 @@ let IS_ADMIN = false;
562
  let AUTH_MODE = 'login';
563
  let SELECTED_VOICE = 'my-MM-ThihaNeural';
564
  let SELECTED_ENGINE = 'ms';
565
- let VO_LANG = 'my'; // 'my' | 'th' | 'en'
566
  let MODE = 'full';
567
  let CUR_OUTPUT_URL = '';
568
  let CUR_CAPTION = '';
@@ -573,28 +484,17 @@ let CURRENT_TID = '';
573
  let SSE_SOURCE = null;
574
 
575
  const MS_V = [
576
- // Myanmar
577
- {id:'my-MM-ThihaNeural', name:'သီဟ', sub:'ကျား — ယုံကြည်မှုရှိ၊ ကြည်လင်', lang:'my'},
578
- {id:'my-MM-NilarNeural', name:'နီလာ', sub:'မိန်း — နူးညံ့၊ သာယာ', lang:'my'},
579
- // Thai
580
- {id:'th-TH-PremwadeeNeural', name:'ပြမ်းဝါဒီ', sub:'မိန်း — နူးညံ့၊ သဘာဝကျ', lang:'th'},
581
- {id:'th-TH-NiwatNeural', name:'နီဝတ်', sub:'ကျား — ပြတ်သား၊ ရှင်းလင်း', lang:'th'},
582
- {id:'th-TH-AcharaNeural', name:'အာချာရာ', sub:'မိန်း — ဖော်ရွေ၊ သက်ဆင်း', lang:'th'},
583
- // English
584
- {id:'en-US-AriaNeural', name:'Aria', sub:'မိန်း — ကြည်လင်၊ သဘာဝကျ', lang:'en'},
585
- {id:'en-US-GuyNeural', name:'Guy', sub:'ကျား — နက်ရှိုင်း၊ ယုံကြည်မှုရှိ', lang:'en'},
586
- {id:'en-US-JennyNeural', name:'Jenny', sub:'မိန်း — ဖော်ရွေ၊ ပူးပေါင်း', lang:'en'},
587
- {id:'en-US-AnaNeural', name:'Ana', sub:'မိန်း — ချိုသာ၊ လတ်ဆတ်', lang:'en'},
588
- {id:'en-US-DavisNeural', name:'Davis', sub:'ကျား — ပျော်ရွှင်၊ တက်ကြွ', lang:'en'},
589
- {id:'en-US-EmmaNeural', name:'Emma', sub:'မိန်း — နူးညံ့၊ သာယာ', lang:'en'},
590
- {id:'en-US-JasonNeural', name:'Jason', sub:'ကျား — နက်ရှိုင်း၊ တည်ငြိမ်', lang:'en'},
591
- {id:'en-US-SaraNeural', name:'Sara', sub:'မိန်း — ကြည်လင်၊ ကျွမ်းကျင်', lang:'en'},
592
- {id:'en-US-TonyNeural', name:'Tony', sub:'ကျား — တက်ကြွ၊ ထက်မြက်', lang:'en'},
593
- {id:'en-GB-SoniaNeural', name:'Sonia', sub:'မိန်း (UK) — ယဉ်ကျေး၊ ရှင်းလင်း', lang:'en'},
594
- {id:'en-GB-RyanNeural', name:'Ryan', sub:'ကျား (UK) — ပြတ်သား၊ ယုံကြည်မှုရှိ',lang:'en'},
595
- {id:'en-GB-LibbyNeural', name:'Libby', sub:'မိန်း (UK) — ဖော်ရွေ၊ သဘာဝကျ', lang:'en'},
596
- {id:'en-GB-MaisieNeural', name:'Maisie', sub:'မိန်း (UK) — ချိုသာ၊ လတ်ဆတ်', lang:'en'},
597
- {id:'en-GB-ThomasNeural', name:'Thomas', sub:'ကျား (UK) — တည်ငြိမ်၊ ကျွမ်းကျင်',lang:'en'},
598
  ];
599
  let GEMINI_V = [];
600
 
@@ -671,7 +571,6 @@ function pasteUrl(){
671
  navigator.clipboard.readText().then(t=>{
672
  document.getElementById('video-url').value = t.trim();
673
  detectPlatform(t.trim());
674
- fetchThumbnail(t.trim());
675
  }).catch(()=>toast('Clipboard access denied'));
676
  }
677
 
@@ -691,12 +590,7 @@ function detectPlatform(url){
691
 
692
  document.addEventListener('DOMContentLoaded', ()=>{
693
  const urlInput = document.getElementById('video-url');
694
- if(urlInput){
695
- urlInput.addEventListener('input', e => {
696
- detectPlatform(e.target.value);
697
- fetchThumbnail(e.target.value);
698
- });
699
- }
700
  });
701
 
702
  function hasVideoInput(){
@@ -712,168 +606,6 @@ function onFileSelect(inp){
712
  }
713
  }
714
  function onMusicSelect(inp){ if(inp.files[0]) document.getElementById('music-name').textContent='🎵 '+inp.files[0].name; }
715
-
716
- // ── LOGO ──
717
- let LOGO_X = 10, LOGO_Y = 10, LOGO_W = 80, LOGO_ENABLED = false;
718
-
719
- function toggleLogo(el){
720
- el.classList.toggle('checked');
721
- LOGO_ENABLED = el.classList.contains('checked');
722
- el.querySelector('.check-box').innerHTML = LOGO_ENABLED ? '<i class="fas fa-check"></i>' : '';
723
- document.getElementById('logo-lbl').textContent = LOGO_ENABLED ? 'On' : 'Off';
724
- document.getElementById('logo-section').style.display = LOGO_ENABLED ? '' : 'none';
725
- }
726
-
727
- function getCropDims(){
728
- const crop = document.getElementById('crop').value;
729
- if(crop==='16:9') return {w:1280, h:720};
730
- if(crop==='1:1') return {w:720, h:720};
731
- return {w:720, h:1280}; // 9:16 and original
732
- }
733
-
734
- function updateLogoPreview(){
735
- const canvas = document.getElementById('logo-preview-canvas');
736
- const img = document.getElementById('logo-drag-img');
737
- if(!img || !img.src) return;
738
- const dims = getCropDims();
739
- // Use canvas offsetWidth (reliable after render) — fallback to 90px
740
- const cw = canvas.offsetWidth || 90;
741
- const ch = canvas.offsetHeight || Math.round(cw * dims.h / dims.w);
742
- // Convert video px → canvas px
743
- const px = Math.round(LOGO_X * cw / dims.w);
744
- const py = Math.round(LOGO_Y * ch / dims.h);
745
- const pw = Math.round(LOGO_W * cw / dims.w);
746
- img.style.left = px + 'px';
747
- img.style.top = py + 'px';
748
- img.style.width = pw + 'px';
749
- }
750
-
751
- function onLogoSelect(inp){
752
- if(!inp.files[0]) return;
753
- document.getElementById('logo-name').textContent = '🖼️ ' + inp.files[0].name;
754
- const img = document.getElementById('logo-drag-img');
755
- img.src = URL.createObjectURL(inp.files[0]);
756
- // Sync bg video
757
- const pv = document.getElementById('preview-video');
758
- const bgv = document.getElementById('logo-bg-video');
759
- if(pv && pv.src) bgv.src = pv.src;
760
- // Set canvas ratio
761
- const dims = getCropDims();
762
- const canvas = document.getElementById('logo-preview-canvas');
763
- canvas.style.aspectRatio = dims.w+'/'+dims.h;
764
- // Update slider max
765
- document.getElementById('logo-x-slider').max = dims.w - LOGO_W;
766
- document.getElementById('logo-y-slider').max = dims.h - LOGO_W;
767
- document.getElementById('logo-pos-wrap').style.display = '';
768
- // Wait for DOM to render canvas before calculating positions
769
- requestAnimationFrame(()=> requestAnimationFrame(updateLogoPreview));
770
- }
771
-
772
- function onLogoSizeChange(val){
773
- LOGO_W = parseInt(val);
774
- document.getElementById('logo-size-val').textContent = val + 'px';
775
- const dims = getCropDims();
776
- document.getElementById('logo-x-slider').max = dims.w - LOGO_W;
777
- document.getElementById('logo-y-slider').max = dims.h - LOGO_W;
778
- updateLogoPreview();
779
- }
780
-
781
- function onLogoPosSlider(){
782
- LOGO_X = parseInt(document.getElementById('logo-x-slider').value);
783
- LOGO_Y = parseInt(document.getElementById('logo-y-slider').value);
784
- document.getElementById('logo-x-val').textContent = LOGO_X;
785
- document.getElementById('logo-y-val').textContent = LOGO_Y;
786
- // Clear active grid btn
787
- document.querySelectorAll('.pos-btn').forEach(b=>b.classList.remove('active'));
788
- updateLogoPreview();
789
- }
790
-
791
- function setLogoPos(pos){
792
- const dims = getCropDims();
793
- const pad = 20;
794
- const positions = {
795
- tl:{x:pad, y:pad},
796
- tc:{x:(dims.w-LOGO_W)/2, y:pad},
797
- tr:{x:dims.w-LOGO_W-pad, y:pad},
798
- ml:{x:pad, y:(dims.h-LOGO_W)/2},
799
- mc:{x:(dims.w-LOGO_W)/2, y:(dims.h-LOGO_W)/2},
800
- mr:{x:dims.w-LOGO_W-pad, y:(dims.h-LOGO_W)/2},
801
- bl:{x:pad, y:dims.h-LOGO_W-pad},
802
- bc:{x:(dims.w-LOGO_W)/2, y:dims.h-LOGO_W-pad},
803
- br:{x:dims.w-LOGO_W-pad, y:dims.h-LOGO_W-pad},
804
- };
805
- const p = positions[pos];
806
- LOGO_X = Math.max(0, Math.round(p.x));
807
- LOGO_Y = Math.max(0, Math.round(p.y));
808
- document.getElementById('logo-x-slider').value = LOGO_X;
809
- document.getElementById('logo-y-slider').value = LOGO_Y;
810
- document.getElementById('logo-x-val').textContent = LOGO_X;
811
- document.getElementById('logo-y-val').textContent = LOGO_Y;
812
- // Highlight active btn
813
- document.querySelectorAll('.pos-btn').forEach(b=>b.classList.remove('active'));
814
- event.target.classList.add('active');
815
- updateLogoPreview();
816
- }
817
-
818
- // Update canvas ratio when crop changes
819
- document.addEventListener('DOMContentLoaded', ()=>{
820
- const cropSel = document.getElementById('crop');
821
- if(cropSel) cropSel.addEventListener('change', ()=>{
822
- const dims = getCropDims();
823
- const canvas = document.getElementById('logo-preview-canvas');
824
- if(canvas) canvas.style.aspectRatio = dims.w+'/'+dims.h;
825
- document.getElementById('logo-x-slider').max = dims.w - LOGO_W;
826
- document.getElementById('logo-y-slider').max = dims.h - LOGO_W;
827
- updateLogoPreview();
828
- });
829
- });
830
-
831
- // ── THUMBNAIL FETCH ──
832
- async function fetchThumbnail(url){
833
- if(!url || !url.startsWith('http')) return;
834
- const thumb = document.getElementById('thumb-preview');
835
-
836
- // YouTube
837
- const yt = url.match(/(?:youtu\.be\/|[?&]v=|shorts\/)([A-Za-z0-9_-]{11})/);
838
- if(yt){
839
- thumb.src = `https://img.youtube.com/vi/${yt[1]}/hqdefault.jpg`;
840
- thumb.style.display='block';
841
- document.getElementById('video-placeholder').style.display='none';
842
- return;
843
- }
844
-
845
- // TikTok — oEmbed
846
- if(/tiktok\.com/i.test(url)){
847
- try{
848
- const r = await fetch(`https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`);
849
- const d = await r.json();
850
- if(d.thumbnail_url){
851
- thumb.src = d.thumbnail_url;
852
- thumb.style.display='block';
853
- document.getElementById('video-placeholder').style.display='none';
854
- return;
855
- }
856
- }catch(e){}
857
- }
858
-
859
- // Facebook — show placeholder icon (FB blocks direct thumbnail access)
860
- if(/facebook\.com|fb\.watch/i.test(url)){
861
- thumb.style.display='none';
862
- const ph = document.getElementById('video-placeholder');
863
- ph.style.display='flex';
864
- ph.innerHTML='<i class="fab fa-facebook" style="font-size:2.5rem;color:#1877f2"></i><p style="font-size:.8rem">Facebook Video</p>';
865
- return;
866
- }
867
-
868
- // Instagram — show placeholder icon
869
- if(/instagram\.com/i.test(url)){
870
- thumb.style.display='none';
871
- const ph = document.getElementById('video-placeholder');
872
- ph.style.display='flex';
873
- ph.innerHTML='<i class="fab fa-instagram" style="font-size:2.5rem;color:#e1306c"></i><p style="font-size:.8rem">Instagram Video</p>';
874
- return;
875
- }
876
- }
877
  function dragOver(e){ e.preventDefault(); document.getElementById('upload-area').classList.add('drag'); }
878
  function dragLeave(){ document.getElementById('upload-area').classList.remove('drag'); }
879
  function dropFile(e){
@@ -892,36 +624,6 @@ function toggleSpeed(){
892
  document.getElementById('speed-row').classList.toggle('visible');
893
  }
894
 
895
- // Default voice + speed per language
896
- const LANG_DEFAULT_VOICE = {
897
- my: {id:'my-MM-ThihaNeural', engine:'ms'},
898
- th: {id:'th-TH-PremwadeeNeural', engine:'ms'},
899
- en: {id:'en-US-AriaNeural', engine:'ms'},
900
- };
901
- const LANG_DEFAULT_SPEED = { my: 30, th: 20, en: 0 };
902
-
903
- function switchLang(lang){
904
- VO_LANG = lang;
905
- ['my','th','en'].forEach(l => document.getElementById('lang-'+l).classList.toggle('active', l===lang));
906
- // Auto-set speed
907
- const spd = LANG_DEFAULT_SPEED[lang] ?? 30;
908
- const slider = document.getElementById('speed-slider');
909
- const valEl = document.getElementById('speed-val');
910
- if(slider){ slider.value = spd; valEl.textContent = spd+'%'; }
911
- // Auto-select default voice for this language (MS tab)
912
- if(VCAT === 'ms' || lang !== 'my'){
913
- if(lang !== 'my'){
914
- VCAT = 'ms'; SELECTED_ENGINE = 'ms';
915
- document.getElementById('vcat-ms').classList.add('active');
916
- document.getElementById('vcat-g').classList.remove('active');
917
- }
918
- const def = LANG_DEFAULT_VOICE[lang];
919
- SELECTED_VOICE = def.id;
920
- SELECTED_ENGINE = def.engine;
921
- }
922
- renderVoices(VCAT);
923
- }
924
-
925
  function switchVCat(c){
926
  VCAT = c;
927
  document.getElementById('vcat-ms').classList.toggle('active', c==='ms');
@@ -929,72 +631,27 @@ function switchVCat(c){
929
  SELECTED_ENGINE = c==='g' ? 'gemini' : 'ms';
930
  if(c==='g' && GEMINI_V.length===0){
931
  fetch('/api/gemini_voices').then(r=>r.json()).then(d=>{
932
- if(d.ok) d.voices.forEach(v=>GEMINI_V.push({id:v.id,name:v.name,sub:'',lang:'my'}));
933
  renderVoices('g');
934
  });
935
  } else renderVoices(c);
936
  }
937
 
938
  function renderVoices(cat){
 
939
  const grid = document.getElementById('voice-grid');
940
  const q = document.getElementById('voice-search').value.toLowerCase();
941
- let voices;
942
- if(cat === 'g'){
943
- voices = GEMINI_V;
944
- } else {
945
- voices = MS_V.filter(v => v.lang === VO_LANG);
946
- }
947
- const filtered = voices.filter(v=>
948
- v.name.toLowerCase().includes(q) ||
949
- v.id.toLowerCase().includes(q) ||
950
- (v.sub||'').toLowerCase().includes(q)
951
- );
952
- const isMS = cat !== 'g';
953
  grid.innerHTML = filtered.map(v=>`
954
- <div class="vcard ${SELECTED_VOICE===v.id?'selected':''}" onclick="selectVoice('${v.id}','${v.lang||'my'}',this)">
955
  <div class="vcard-name">${v.name}</div>
956
- <div class="vcard-sub">${v.sub||''}</div>
957
- ${isMS ? `<div class="vcard-play" id="play-${v.id.replace(/[^a-z0-9]/gi,'_')}" onclick="event.stopPropagation();previewVoice('${v.id}',this)"><i class="fas fa-play" style="font-size:.55rem"></i> နားထောင်</div>` : ''}
958
  </div>`).join('');
959
  }
960
 
961
  function filterVoices(q){ renderVoices(VCAT); }
962
-
963
- let _previewAudio = null;
964
- async function previewVoice(voiceId, btnEl){
965
- // Stop any playing preview
966
- if(_previewAudio){ _previewAudio.pause(); _previewAudio = null; }
967
- document.querySelectorAll('.vcard-play').forEach(b=>{
968
- b.classList.remove('playing');
969
- b.innerHTML = '<i class="fas fa-play" style="font-size:.55rem"></i> နားထောင်';
970
- });
971
- btnEl.classList.add('playing');
972
- btnEl.innerHTML = '<i class="fas fa-spinner spinning" style="font-size:.55rem"></i> Loading…';
973
- try {
974
- const spd = document.getElementById('speed-slider').value;
975
- const r = await fetch('/api/preview_voice', {
976
- method:'POST', headers:{'Content-Type':'application/json'},
977
- body: JSON.stringify({voice: voiceId, speed: parseInt(spd), engine:'ms'})
978
- });
979
- const d = await r.json();
980
- if(!d.ok){ toast('❌ Preview failed'); btnEl.classList.remove('playing'); btnEl.innerHTML='<i class="fas fa-play" style="font-size:.55rem"></i> နားထောင်'; return; }
981
- _previewAudio = new Audio(d.url);
982
- _previewAudio.play();
983
- btnEl.innerHTML = '<i class="fas fa-volume-up" style="font-size:.55rem"></i> ဖွင့်နေ…';
984
- _previewAudio.onended = ()=>{
985
- btnEl.classList.remove('playing');
986
- btnEl.innerHTML = '<i class="fas fa-play" style="font-size:.55rem"></i> နားထောင်';
987
- _previewAudio = null;
988
- };
989
- } catch(e){
990
- toast('❌ '+e);
991
- btnEl.classList.remove('playing');
992
- btnEl.innerHTML = '<i class="fas fa-play" style="font-size:.55rem"></i> နားထောင်';
993
- }
994
- }
995
- function selectVoice(id, lang, el){
996
  SELECTED_VOICE = id;
997
- if(lang) VO_LANG = lang;
998
  document.querySelectorAll('.vcard').forEach(c=>c.classList.remove('selected'));
999
  el.classList.add('selected');
1000
  }
@@ -1015,7 +672,6 @@ function buildFormData(includeScript){
1015
  fd.append('username', CUR_USER);
1016
  fd.append('voice', SELECTED_VOICE);
1017
  fd.append('engine', SELECTED_ENGINE);
1018
- fd.append('vo_lang', VO_LANG);
1019
  fd.append('speed', document.getElementById('speed-slider').value);
1020
  fd.append('crop', document.getElementById('crop').value);
1021
  fd.append('flip', isChecked('chk-fl') ? '1' : '0');
@@ -1032,16 +688,6 @@ function buildFormData(includeScript){
1032
  }
1033
  const mf = document.getElementById('music-file').files[0];
1034
  if(mf) fd.append('music_file', mf);
1035
- // Logo — only if enabled
1036
- if(LOGO_ENABLED){
1037
- const lf = document.getElementById('logo-file').files[0];
1038
- if(lf){
1039
- fd.append('logo_file', lf);
1040
- fd.append('logo_x', LOGO_X);
1041
- fd.append('logo_y', LOGO_Y);
1042
- fd.append('logo_w', LOGO_W);
1043
- }
1044
- }
1045
  if(includeScript){
1046
  const sc = document.getElementById('script-in').value.trim();
1047
  if(sc) fd.append('script', sc);
@@ -1057,7 +703,7 @@ async function doProcessAll(){
1057
  fd.append('tid', CURRENT_TID);
1058
  try {
1059
  const ctrl = new AbortController();
1060
- const timer = setTimeout(()=>ctrl.abort(), 1200000); // 20 minutes
1061
  const r = await fetch('/api/process_all',{method:'POST',body:fd,signal:ctrl.signal});
1062
  clearTimeout(timer);
1063
  const d = await r.json();
@@ -1137,10 +783,6 @@ function showPreviewVideo(url){
1137
  const v = document.getElementById('preview-video');
1138
  v.src = url; v.style.display='block';
1139
  document.getElementById('video-placeholder').style.display='none';
1140
- document.getElementById('thumb-preview').style.display='none';
1141
- // Sync logo bg video if logo is set
1142
- const bgv = document.getElementById('logo-bg-video');
1143
- if(bgv) bgv.src = url;
1144
  }
1145
 
1146
  function downloadVideo(){
 
95
  .vcard:hover{border-color:var(--border2);background:var(--bg2)}
96
  .vcard.selected{border-color:var(--amber);background:rgba(245,166,35,.06)}
97
  .vcard-name{font-size:.72rem;font-weight:600;margin-bottom:2px;line-height:1.2}
98
+ .vcard-sub{font-size:.6rem;color:var(--muted);margin-bottom:6px}
99
+ .vcard-play{display:none !important}
 
 
 
100
 
101
  /* ── SPEED (hidden until toggle) ── */
102
  .speed-toggle{display:flex;align-items:center;gap:6px;margin-top:10px;cursor:pointer;font-size:.78rem;color:var(--muted2);font-weight:500;user-select:none}
 
148
  .preview-top{padding:12px 14px;border-bottom:1px solid var(--border);font-size:.68rem;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:6px}
149
  .preview-top i{color:var(--amber)}
150
  .video-wrap{aspect-ratio:9/16;background:#f0f1f4;position:relative;max-height:400px;overflow:hidden}
151
+ .video-wrap video{width:100%;height:100%;object-fit:contain}
152
  .video-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--muted)}
153
  .video-placeholder i{font-size:2.5rem}
154
  .video-placeholder p{font-size:.8rem}
 
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>
 
326
  <!-- SETTINGS -->
327
  <div class="card">
328
  <div class="card-label"><i class="fas fa-cog"></i> SETTINGS</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  <div class="grid2">
330
  <div>
331
  <div class="field-label"><i class="fas fa-crop-alt"></i> Crop Ratio</div>
 
366
  <span style="font-size:.8rem;color:var(--muted2)"><i class="fas fa-music"></i> <span id="music-name">Choose MP3</span></span>
367
  </div>
368
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  </div>
370
 
371
  <div id="action-full">
 
388
  <div class="preview-panel">
389
  <div class="preview-box">
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">
 
397
  <div class="meta-tags" id="meta-tags" style="display:none"></div>
398
  <button class="download-btn" id="download-btn" onclick="downloadVideo()"><i class="fas fa-download"></i> Download MP4</button>
399
  <button class="copy-caption-btn" id="copy-caption-btn" onclick="copyCaption()"><i class="fas fa-copy"></i> Copy Caption</button>
 
400
  <button class="download-btn" id="download-btn2" onclick="downloadVideo()" style="display:none;margin-top:6px;background:rgba(9,132,227,.06);border-color:rgba(9,132,227,.2);color:var(--cyan)"><i class="fas fa-link"></i> Open Link</button>
401
  </div>
402
  </div>
 
474
  let AUTH_MODE = 'login';
475
  let SELECTED_VOICE = 'my-MM-ThihaNeural';
476
  let SELECTED_ENGINE = 'ms';
 
477
  let MODE = 'full';
478
  let CUR_OUTPUT_URL = '';
479
  let CUR_CAPTION = '';
 
484
  let SSE_SOURCE = null;
485
 
486
  const MS_V = [
487
+ {id:'my-MM-ThihaNeural',name:'Thiha',sub:'Male'},
488
+ {id:'my-MM-NilarNeural',name:'Nilar',sub:'Female'},
489
+ {id:'en-US-AriaNeural',name:'Aria',sub:'EN Female'},
490
+ {id:'en-US-GuyNeural',name:'Guy',sub:'EN Male'},
491
+ {id:'en-US-JennyNeural',name:'Jenny',sub:'EN Female'},
492
+ {id:'en-GB-SoniaNeural',name:'Sonia',sub:'UK Female'},
493
+ {id:'en-GB-RyanNeural',name:'Ryan',sub:'UK Male'},
494
+ {id:'zh-CN-XiaoxiaoNeural',name:'Xiaoxiao',sub:'CN Female'},
495
+ {id:'ja-JP-NanamiNeural',name:'Nanami',sub:'JP Female'},
496
+ {id:'ko-KR-SunHiNeural',name:'SunHi',sub:'KR Female'},
497
+ {id:'th-TH-PremwadeeNeural',name:'Premwadee',sub:'TH Female'},
 
 
 
 
 
 
 
 
 
 
 
498
  ];
499
  let GEMINI_V = [];
500
 
 
571
  navigator.clipboard.readText().then(t=>{
572
  document.getElementById('video-url').value = t.trim();
573
  detectPlatform(t.trim());
 
574
  }).catch(()=>toast('Clipboard access denied'));
575
  }
576
 
 
590
 
591
  document.addEventListener('DOMContentLoaded', ()=>{
592
  const urlInput = document.getElementById('video-url');
593
+ if(urlInput) urlInput.addEventListener('input', e => detectPlatform(e.target.value));
 
 
 
 
 
594
  });
595
 
596
  function hasVideoInput(){
 
606
  }
607
  }
608
  function onMusicSelect(inp){ if(inp.files[0]) document.getElementById('music-name').textContent='🎵 '+inp.files[0].name; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  function dragOver(e){ e.preventDefault(); document.getElementById('upload-area').classList.add('drag'); }
610
  function dragLeave(){ document.getElementById('upload-area').classList.remove('drag'); }
611
  function dropFile(e){
 
624
  document.getElementById('speed-row').classList.toggle('visible');
625
  }
626
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  function switchVCat(c){
628
  VCAT = c;
629
  document.getElementById('vcat-ms').classList.toggle('active', c==='ms');
 
631
  SELECTED_ENGINE = c==='g' ? 'gemini' : 'ms';
632
  if(c==='g' && GEMINI_V.length===0){
633
  fetch('/api/gemini_voices').then(r=>r.json()).then(d=>{
634
+ if(d.ok) d.voices.forEach(v=>GEMINI_V.push({id:v.id,name:v.name,sub:''}));
635
  renderVoices('g');
636
  });
637
  } else renderVoices(c);
638
  }
639
 
640
  function renderVoices(cat){
641
+ const voices = cat==='g' ? GEMINI_V : MS_V;
642
  const grid = document.getElementById('voice-grid');
643
  const q = document.getElementById('voice-search').value.toLowerCase();
644
+ const filtered = voices.filter(v=>v.name.toLowerCase().includes(q)||v.id.toLowerCase().includes(q));
 
 
 
 
 
 
 
 
 
 
 
645
  grid.innerHTML = filtered.map(v=>`
646
+ <div class="vcard ${SELECTED_VOICE===v.id?'selected':''}" onclick="selectVoice('${v.id}',this)">
647
  <div class="vcard-name">${v.name}</div>
648
+ <div class="vcard-sub">${v.sub}</div>
 
649
  </div>`).join('');
650
  }
651
 
652
  function filterVoices(q){ renderVoices(VCAT); }
653
+ function selectVoice(id, el){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  SELECTED_VOICE = id;
 
655
  document.querySelectorAll('.vcard').forEach(c=>c.classList.remove('selected'));
656
  el.classList.add('selected');
657
  }
 
672
  fd.append('username', CUR_USER);
673
  fd.append('voice', SELECTED_VOICE);
674
  fd.append('engine', SELECTED_ENGINE);
 
675
  fd.append('speed', document.getElementById('speed-slider').value);
676
  fd.append('crop', document.getElementById('crop').value);
677
  fd.append('flip', isChecked('chk-fl') ? '1' : '0');
 
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);
 
703
  fd.append('tid', CURRENT_TID);
704
  try {
705
  const ctrl = new AbortController();
706
+ const timer = setTimeout(()=>ctrl.abort(), 600000);
707
  const r = await fetch('/api/process_all',{method:'POST',body:fd,signal:ctrl.signal});
708
  clearTimeout(timer);
709
  const d = await r.json();
 
783
  const v = document.getElementById('preview-video');
784
  v.src = url; v.style.display='block';
785
  document.getElementById('video-placeholder').style.display='none';
 
 
 
 
786
  }
787
 
788
  function downloadVideo(){
m_youtube_com_cookies.txt CHANGED
@@ -1,6 +1,5 @@
1
  # Netscape HTTP Cookie File
2
- # https://curl.haxx.se/rfc/cookie_spec.html
3
- # This is a generated file! Do not edit.
4
 
5
  .youtube.com TRUE / FALSE 1807419670 HSID AbIQP2u9sl_Bnx0g5
6
  .youtube.com TRUE / TRUE 1807419670 SSID ARUUfbNPh92AKO5_p
@@ -11,10 +10,18 @@
11
  .youtube.com TRUE / FALSE 1807419670 SID g.a0007ghOD_dtmZYRBcsFCcLlSjCZt58NwNnryfaNOHad_EUw9JsrZxi6iorwIQ3xtfFXcSuVXwACgYKAQwSARASFQHGX2MiVxcnmRJO7uOlwy3PxCDXGhoVAUF8yKpHQUl7lgYfpi0qGObLsX5H0076
12
  .youtube.com TRUE / TRUE 1807419670 __Secure-1PSID g.a0007ghOD_dtmZYRBcsFCcLlSjCZt58NwNnryfaNOHad_EUw9JsrQVE4_qQpYPr784enWmbf7wACgYKAbcSARASFQHGX2MiSMcqGdcCalYjuRGFq5LbVBoVAUF8yKqVQF-sliTQkRIyb1XDzw5X0076
13
  .youtube.com TRUE / TRUE 1807419670 __Secure-3PSID g.a0007ghOD_dtmZYRBcsFCcLlSjCZt58NwNnryfaNOHad_EUw9JsrzqOS36tWU0CIXs3cerrxeAACgYKAe4SARASFQHGX2Mi7VHrSfHLUV6tErWLzpV4nBoVAUF8yKrHu6T_gUNfkXQSwf0xQ5Il0076
14
- .youtube.com TRUE / TRUE 1807712261 PREF f6=40000000&tz=Asia.Bangkok&f4=4000000
15
  .youtube.com TRUE / TRUE 1804395670 __Secure-1PSIDTS sidts-CjUBBj1CYjBzrPiWsjOc3UdOv7GeV3lhd7EW9D_OutE1KkfvsmhjH4FnEkdDEtzcYGrD_o-GvBAA
16
  .youtube.com TRUE / TRUE 1804395670 __Secure-3PSIDTS sidts-CjUBBj1CYjBzrPiWsjOc3UdOv7GeV3lhd7EW9D_OutE1KkfvsmhjH4FnEkdDEtzcYGrD_o-GvBAA
17
  .youtube.com TRUE / TRUE 1807419670 LOGIN_INFO AFmmF2swRQIhAJZITc_-ARiB1DrpXF_oTjarOxRospGJWcB_aSYLHSFDAiA9SlGWqEXXI_scn8UYzqO1-WDrV9HYqH9cjNixTgOydA:QUQ3MjNmeWNJNTUxeE83czJKelU2WnR3MUxTbE96OV96VVdUUm5BSHFkX0JWRWczMm5UVzNMaE5nQ3Nrd01nMmJNOV9paVlQTlV5ZG5WRnQ5WjJUdWlpM0lUbDdRYmJ3Z2NvVHNkU1BRQ3l5NlR0Rm5yWFI3NjhQOWotLVJGdDc4aUN3THA4UENjMUJBc1c1Um1GRWRMVzROb1VXSmNLVUFn
18
- .youtube.com TRUE / FALSE 1804688263 SIDCC AKEyXzU13ySVOGCeOSjt73eXhS8qaaqmd5CrLxlM0uvg95O6CLX0OfWnUwrrzcWUD-fFtMan2g
19
- .youtube.com TRUE / TRUE 1804688263 __Secure-1PSIDCC AKEyXzXL0CTf_TylwzSvKfzFPxUCCY_OiWwpN4bQtBEfoZ4F_B7Gzv7Ohkq6iMLYwbf9x9zRAA
20
- .youtube.com TRUE / TRUE 1804688263 __Secure-3PSIDCC AKEyXzV5nXpPRLhuz8xsQtvnN5sIzyZq7RtEx0QxTBHLb1c_8yPNKFWBL9m00hMjsjcd_1gJ
 
 
 
 
 
 
 
 
 
1
  # Netscape HTTP Cookie File
2
+ # This file is generated by yt-dlp. Do not edit.
 
3
 
4
  .youtube.com TRUE / FALSE 1807419670 HSID AbIQP2u9sl_Bnx0g5
5
  .youtube.com TRUE / TRUE 1807419670 SSID ARUUfbNPh92AKO5_p
 
10
  .youtube.com TRUE / FALSE 1807419670 SID g.a0007ghOD_dtmZYRBcsFCcLlSjCZt58NwNnryfaNOHad_EUw9JsrZxi6iorwIQ3xtfFXcSuVXwACgYKAQwSARASFQHGX2MiVxcnmRJO7uOlwy3PxCDXGhoVAUF8yKpHQUl7lgYfpi0qGObLsX5H0076
11
  .youtube.com TRUE / TRUE 1807419670 __Secure-1PSID g.a0007ghOD_dtmZYRBcsFCcLlSjCZt58NwNnryfaNOHad_EUw9JsrQVE4_qQpYPr784enWmbf7wACgYKAbcSARASFQHGX2MiSMcqGdcCalYjuRGFq5LbVBoVAUF8yKqVQF-sliTQkRIyb1XDzw5X0076
12
  .youtube.com TRUE / TRUE 1807419670 __Secure-3PSID g.a0007ghOD_dtmZYRBcsFCcLlSjCZt58NwNnryfaNOHad_EUw9JsrzqOS36tWU0CIXs3cerrxeAACgYKAe4SARASFQHGX2Mi7VHrSfHLUV6tErWLzpV4nBoVAUF8yKrHu6T_gUNfkXQSwf0xQ5Il0076
13
+ .youtube.com TRUE / FALSE 0 PREF f6=40000000&tz=UTC&f4=4000000&hl=en
14
  .youtube.com TRUE / TRUE 1804395670 __Secure-1PSIDTS sidts-CjUBBj1CYjBzrPiWsjOc3UdOv7GeV3lhd7EW9D_OutE1KkfvsmhjH4FnEkdDEtzcYGrD_o-GvBAA
15
  .youtube.com TRUE / TRUE 1804395670 __Secure-3PSIDTS sidts-CjUBBj1CYjBzrPiWsjOc3UdOv7GeV3lhd7EW9D_OutE1KkfvsmhjH4FnEkdDEtzcYGrD_o-GvBAA
16
  .youtube.com TRUE / TRUE 1807419670 LOGIN_INFO AFmmF2swRQIhAJZITc_-ARiB1DrpXF_oTjarOxRospGJWcB_aSYLHSFDAiA9SlGWqEXXI_scn8UYzqO1-WDrV9HYqH9cjNixTgOydA:QUQ3MjNmeWNJNTUxeE83czJKelU2WnR3MUxTbE96OV96VVdUUm5BSHFkX0JWRWczMm5UVzNMaE5nQ3Nrd01nMmJNOV9paVlQTlV5ZG5WRnQ5WjJUdWlpM0lUbDdRYmJ3Z2NvVHNkU1BRQ3l5NlR0Rm5yWFI3NjhQOWotLVJGdDc4aUN3THA4UENjMUJBc1c1Um1GRWRMVzROb1VXSmNLVUFn
17
+ .youtube.com TRUE / FALSE 1805100407 SIDCC AKEyXzUZzXYE9q1sGPyrjTzRU7or3ztQoOWmWUjpNpD4KWkkRjVg90OhszsHFH3vA_z-wIJp0g
18
+ .youtube.com TRUE / TRUE 1805100407 __Secure-1PSIDCC AKEyXzU7YqlW3ainCEwhll9c3ugmwBmlzZ2BEuUHW3XBI7wfpBub3H4EvE7MGoneo0d66sTcDg
19
+ .youtube.com TRUE / TRUE 1805100407 __Secure-3PSIDCC AKEyXzWw9LKhoM1BEWg7Qe4CFaLhowWakCCvZMu_UwyjJGzPlpCrzK2VC7C-4KRQ6atHU8mR
20
+ .youtube.com TRUE / TRUE 1807673124 __Secure-YENID 14.YTE=ktodmcvCvot2KhGLrHdMFIH43XujYTwrvpNFERZRqCL9vzfb6ocOX7XbgDhWdGX_4XW7W17zfAQqERP2mdJ10BSnyTvEjz0DGFmNN9mbg7pE6s9pwn7B_PGnIDkPzVKbBAWg2tOks6U1HvG6JLUCDTdjGzEd3G9IEOs-ec-wvLUDwdNzyrN19BjNPpOGK98fSgKmnRs7jLYv6t4HpF-XXegyORmjlUjPLfFwKyZbOps24BJZZI3fZZl2aIzA4KCWptYWK28q-fvohFQktrpf-gQ0XRfRlvL8TFBGaEivG0Y-e-S62HJMVPmvOicZ_cBKkko1qDjNfm-mibsIYGMrRw
21
+ .youtube.com TRUE / TRUE 0 YSC plPg_tapgO8
22
+ .youtube.com TRUE / TRUE 1807673123 __Secure-YEC CgtFczM2T1MtZnExQSj349nNBjIKCgJERRIEEgAgRmLgAgrdAjE0LllURT1rdG9kbWN2Q3ZvdDJLaEdMckhkTUZJSDQzWHVqWVR3cnZwTkZFUlpScUNMOXZ6ZmI2b2NPWDdYYmdEaFdkR1hfNFhXN1cxN3pmQVFxRVJQMm1kSjEwQlNueVR2RWp6MERHRm1OTjltYmc3cEU2czlwd243Ql9QR25JRGtQelZLYkJBV2cydE9rczZVMUh2RzZKTFVDRFRkakd6RWQzRzlJRU9zLWVjLXd2TFVEd2ROenlyTjE5QmpOUHBPR0s5OGZTZ0ttblJzN2pMWXY2dDRIcEYtWFhlZ3lPUm1qbFVqUExmRndLeVpiT3BzMjRCSlpaSTNmWlpsMmFJekE0S0NXcHRZV0syOHEtZnZvaEZRa3RycGYtZ1EwWFJmUmx2TDhURkJHYUVpdkcwWS1lLVM2MkhKTVZQbXZPaWNaX2NCS2trbzFxRGpOZm0tbWlic0lZR01yUnc%3D
23
+ .youtube.com TRUE / TRUE 1807692407 VISITOR_PRIVACY_METADATA CgJERRIEEgAgRg%3D%3D
24
+ .youtube.com TRUE / TRUE 1789097604 __Secure-ROLLOUT_TOKEN CNTe-8e85Jn_UhCH8_C4-qCTAxj0w96d_KCTAw%3D%3D
25
+ .tiktok.com TRUE / TRUE 1804684903 ttwid 1%7ConRsRaYlTEnh2HeZGgD-iXMNUyODIy3OiTvoUcZY6-k%7C1773580903%7C47fc4402a79fa31a5885a8bdd1f706c6ca7126f1b387eb59fc48c4b42fcb54e6
26
+ .tiktok.com TRUE / TRUE 0 tt_csrf_token BVfV0iS0-5PUImEsPdknbJLmIUh5IvqyE2VA
27
+ .tiktok.com TRUE / TRUE 1789136617 tt_chain_token 9sXx+RxqzEtBf+kc7+7XuA==