moonlantern1 commited on
Commit
9c19b67
·
verified ·
1 Parent(s): e073547

Fix native upload input and YouTube TLS impersonation

Browse files
Files changed (3) hide show
  1. app.py +29 -8
  2. pyproject.toml +1 -0
  3. src/humeo/ingest.py +28 -1
app.py CHANGED
@@ -470,9 +470,11 @@ INDEX_HTML = r"""<!DOCTYPE html>
470
  .input-label { font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); margin-bottom: 8px; display: block; font-weight: 500; text-align:left; }
471
  .yt-input { width: 100%; padding: 14px 16px; border: 1.5px solid var(--border); border-radius: var(--radius); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; background: var(--cream); color: var(--ink); outline: none; transition: border-color 0.2s; }
472
  .yt-input:focus { border-color: var(--gold); } .yt-input::placeholder { color: var(--ink-muted); }
473
- .upload-zone { border: 2px dashed var(--champagne-deep); border-radius: var(--radius); padding: 36px 20px; text-align: center; cursor: pointer; transition: all 0.2s; background: var(--cream); }
 
 
 
474
  .upload-zone:hover, .upload-zone.dragover { border-color: var(--gold); background: var(--champagne); }
475
- .file-input-visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; pointer-events: none; }
476
  .upload-icon { width: 44px; height: 44px; background: var(--champagne); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; font-size: 1.2rem; }
477
  .upload-text { font-size: 0.9rem; color: var(--ink-soft); font-weight: 400; }
478
  .upload-sub { font-size: 0.78rem; color: var(--ink-muted); margin-top: 4px; }
@@ -564,12 +566,12 @@ INDEX_HTML = r"""<!DOCTYPE html>
564
  <input class="yt-input" type="text" placeholder="https://youtube.com/watch?v=..." id="yt-url">
565
  </div>
566
  <div class="input-section" id="mode-upload">
567
- <input class="file-input-visually-hidden" type="file" id="file-input" accept="video/mp4,video/quicktime,video/*">
568
- <label class="upload-zone" id="upload-zone" for="file-input">
569
  <div class="upload-icon">File</div>
570
  <div class="upload-text">Click to browse or drag & drop</div>
571
  <div class="upload-sub">MP4, MOV, AVI - up to your Space limit</div>
572
- </label>
573
  </div>
574
  <button class="convert-btn" id="convert-btn" onclick="startProcessing()">Convert to Clips -></button>
575
  </div>
@@ -626,6 +628,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
626
  <script>
627
  let currentMode = 'yt';
628
  let selectedFile = null;
 
629
  let currentJobId = null;
630
  let renderedClips = [];
631
  const iconLabels = ['Up','Text','Cut','Film','Edit'];
@@ -640,13 +643,22 @@ INDEX_HTML = r"""<!DOCTYPE html>
640
  function openUpload() { document.getElementById('file-input').click(); }
641
 
642
  function setSelectedFile(file) {
 
643
  selectedFile = file;
644
  const zone = document.getElementById('upload-zone');
645
  zone.innerHTML = `<div class="upload-icon">OK</div><div class="upload-text" style="color:var(--gold)">File selected: ${escapeHtml(file.name)}</div><div class="upload-sub">Ready to convert</div>`;
646
  }
647
 
648
  const uploadZone = document.getElementById('upload-zone');
649
- document.getElementById('file-input').addEventListener('change', e => { if (e.target.files[0]) setSelectedFile(e.target.files[0]); });
 
 
 
 
 
 
 
 
650
  uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
651
  uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
652
  uploadZone.addEventListener('drop', e => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files[0]) setSelectedFile(e.dataTransfer.files[0]); });
@@ -661,8 +673,9 @@ INDEX_HTML = r"""<!DOCTYPE html>
661
  form.append('source_job_id', currentJobId);
662
  form.append('regen_prompt', extraPrompt);
663
  } else if (currentMode === 'upload') {
664
- if (!selectedFile) throw new Error('Choose a video file first.');
665
- form.append('file', selectedFile);
 
666
  } else {
667
  const url = document.getElementById('yt-url').value.trim();
668
  if (!url) throw new Error('Paste a video URL first.');
@@ -677,6 +690,14 @@ INDEX_HTML = r"""<!DOCTYPE html>
677
  async function startProcessing() {
678
  const btn = document.getElementById('convert-btn');
679
  try {
 
 
 
 
 
 
 
 
680
  btn.disabled = true;
681
  btn.textContent = 'Starting...';
682
  const job = await createJob();
 
470
  .input-label { font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); margin-bottom: 8px; display: block; font-weight: 500; text-align:left; }
471
  .yt-input { width: 100%; padding: 14px 16px; border: 1.5px solid var(--border); border-radius: var(--radius); font-family: 'DM Sans', sans-serif; font-size: 0.9rem; background: var(--cream); color: var(--ink); outline: none; transition: border-color 0.2s; }
472
  .yt-input:focus { border-color: var(--gold); } .yt-input::placeholder { color: var(--ink-muted); }
473
+ .native-file-input { width: 100%; padding: 12px; margin-bottom: 12px; border: 1.5px solid var(--border); border-radius: var(--radius); background: var(--cream); color: var(--ink-soft); font-family: 'DM Sans', sans-serif; font-size: 0.86rem; }
474
+ .native-file-input::file-selector-button { margin-right: 12px; padding: 9px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--white); color: var(--ink); font-family: 'DM Sans', sans-serif; cursor: pointer; }
475
+ .native-file-input::file-selector-button:hover { background: var(--champagne); }
476
+ .upload-zone { border: 2px dashed var(--champagne-deep); border-radius: var(--radius); padding: 28px 20px; text-align: center; cursor: pointer; transition: all 0.2s; background: var(--cream); }
477
  .upload-zone:hover, .upload-zone.dragover { border-color: var(--gold); background: var(--champagne); }
 
478
  .upload-icon { width: 44px; height: 44px; background: var(--champagne); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; font-size: 1.2rem; }
479
  .upload-text { font-size: 0.9rem; color: var(--ink-soft); font-weight: 400; }
480
  .upload-sub { font-size: 0.78rem; color: var(--ink-muted); margin-top: 4px; }
 
566
  <input class="yt-input" type="text" placeholder="https://youtube.com/watch?v=..." id="yt-url">
567
  </div>
568
  <div class="input-section" id="mode-upload">
569
+ <input class="native-file-input" type="file" id="file-input" accept="video/mp4,video/quicktime,video/*">
570
+ <div class="upload-zone" id="upload-zone" onclick="openUpload()">
571
  <div class="upload-icon">File</div>
572
  <div class="upload-text">Click to browse or drag & drop</div>
573
  <div class="upload-sub">MP4, MOV, AVI - up to your Space limit</div>
574
+ </div>
575
  </div>
576
  <button class="convert-btn" id="convert-btn" onclick="startProcessing()">Convert to Clips -></button>
577
  </div>
 
628
  <script>
629
  let currentMode = 'yt';
630
  let selectedFile = null;
631
+ let autoStartAfterFilePick = false;
632
  let currentJobId = null;
633
  let renderedClips = [];
634
  const iconLabels = ['Up','Text','Cut','Film','Edit'];
 
643
  function openUpload() { document.getElementById('file-input').click(); }
644
 
645
  function setSelectedFile(file) {
646
+ if (!file) return;
647
  selectedFile = file;
648
  const zone = document.getElementById('upload-zone');
649
  zone.innerHTML = `<div class="upload-icon">OK</div><div class="upload-text" style="color:var(--gold)">File selected: ${escapeHtml(file.name)}</div><div class="upload-sub">Ready to convert</div>`;
650
  }
651
 
652
  const uploadZone = document.getElementById('upload-zone');
653
+ document.getElementById('file-input').addEventListener('change', e => {
654
+ if (e.target.files[0]) {
655
+ setSelectedFile(e.target.files[0]);
656
+ if (autoStartAfterFilePick) {
657
+ autoStartAfterFilePick = false;
658
+ startProcessing();
659
+ }
660
+ }
661
+ });
662
  uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
663
  uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
664
  uploadZone.addEventListener('drop', e => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files[0]) setSelectedFile(e.dataTransfer.files[0]); });
 
673
  form.append('source_job_id', currentJobId);
674
  form.append('regen_prompt', extraPrompt);
675
  } else if (currentMode === 'upload') {
676
+ const file = selectedFile || document.getElementById('file-input').files[0];
677
+ if (!file) throw new Error('Choose a video file first.');
678
+ form.append('file', file);
679
  } else {
680
  const url = document.getElementById('yt-url').value.trim();
681
  if (!url) throw new Error('Paste a video URL first.');
 
690
  async function startProcessing() {
691
  const btn = document.getElementById('convert-btn');
692
  try {
693
+ if (currentMode === 'upload') {
694
+ selectedFile = selectedFile || document.getElementById('file-input').files[0] || null;
695
+ if (!selectedFile) {
696
+ autoStartAfterFilePick = true;
697
+ openUpload();
698
+ return;
699
+ }
700
+ }
701
  btn.disabled = true;
702
  btn.textContent = 'Starting...';
703
  const job = await createJob();
pyproject.toml CHANGED
@@ -14,6 +14,7 @@ dependencies = [
14
  "openai>=1.0",
15
  "google-genai>=1.0",
16
  "httpx>=0.28",
 
17
  "jinja2>=3.1",
18
  "numpy>=1.24",
19
  "Pillow>=10.0",
 
14
  "openai>=1.0",
15
  "google-genai>=1.0",
16
  "httpx>=0.28",
17
+ "curl_cffi>=0.10,<0.15",
18
  "jinja2>=3.1",
19
  "numpy>=1.24",
20
  "Pillow>=10.0",
src/humeo/ingest.py CHANGED
@@ -72,6 +72,22 @@ def _yt_dlp_cookie_file(output_dir: Path) -> Path | None:
72
  return cookie_path
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def _yt_dlp_error(exc: subprocess.CalledProcessError) -> RuntimeError:
76
  stdout = (exc.stdout or "").strip()
77
  stderr = (exc.stderr or "").strip()
@@ -84,6 +100,12 @@ def _yt_dlp_error(exc: subprocess.CalledProcessError) -> RuntimeError:
84
  "YTDLP_COOKIES_B64 containing a base64 encoded Netscape cookies.txt export "
85
  "from a logged-in browser, or upload the MP4 directly."
86
  )
 
 
 
 
 
 
87
  return RuntimeError(f"yt-dlp failed to download the YouTube video:\n{details}{hint}")
88
 
89
 
@@ -146,13 +168,18 @@ def download_video(youtube_url: str, output_dir: Path) -> Path:
146
  "3",
147
  "--socket-timeout",
148
  "30",
149
- "--force-ipv4",
150
  "--user-agent",
151
  YTDLP_BROWSER_USER_AGENT,
152
  "--extractor-args",
153
  (os.environ.get("YTDLP_EXTRACTOR_ARGS") or "youtube:player_client=default,web_creator"),
154
  "--quiet",
155
  ]
 
 
 
 
 
 
156
  if shutil.which("node"):
157
  cmd.extend(["--js-runtimes", "node", "--remote-components", "ejs:github"])
158
  cookie_path = _yt_dlp_cookie_file(output_dir)
 
72
  return cookie_path
73
 
74
 
75
+ def _yt_dlp_impersonate_target() -> str | None:
76
+ target = (os.environ.get("YTDLP_IMPERSONATE") or "chrome").strip()
77
+ if target.lower() in {"", "0", "false", "no", "off", "none"}:
78
+ return None
79
+ return target
80
+
81
+
82
+ def _yt_dlp_ip_family_flag() -> str | None:
83
+ value = (os.environ.get("YTDLP_IP_FAMILY") or "").strip().lower()
84
+ if value in {"4", "ipv4"}:
85
+ return "--force-ipv4"
86
+ if value in {"6", "ipv6"}:
87
+ return "--force-ipv6"
88
+ return None
89
+
90
+
91
  def _yt_dlp_error(exc: subprocess.CalledProcessError) -> RuntimeError:
92
  stdout = (exc.stdout or "").strip()
93
  stderr = (exc.stderr or "").strip()
 
100
  "YTDLP_COOKIES_B64 containing a base64 encoded Netscape cookies.txt export "
101
  "from a logged-in browser, or upload the MP4 directly."
102
  )
103
+ elif "unexpected_eof_while_reading" in lowered or "ssl" in lowered:
104
+ hint = (
105
+ "\n\nYouTube closed the TLS connection from Hugging Face. The app will use "
106
+ "browser TLS impersonation when curl_cffi is installed; if this persists, "
107
+ "upload the MP4 directly or add YTDLP_COOKIES_B64."
108
+ )
109
  return RuntimeError(f"yt-dlp failed to download the YouTube video:\n{details}{hint}")
110
 
111
 
 
168
  "3",
169
  "--socket-timeout",
170
  "30",
 
171
  "--user-agent",
172
  YTDLP_BROWSER_USER_AGENT,
173
  "--extractor-args",
174
  (os.environ.get("YTDLP_EXTRACTOR_ARGS") or "youtube:player_client=default,web_creator"),
175
  "--quiet",
176
  ]
177
+ ip_family_flag = _yt_dlp_ip_family_flag()
178
+ if ip_family_flag:
179
+ cmd.append(ip_family_flag)
180
+ impersonate_target = _yt_dlp_impersonate_target()
181
+ if impersonate_target:
182
+ cmd.extend(["--impersonate", impersonate_target])
183
  if shutil.which("node"):
184
  cmd.extend(["--js-runtimes", "node", "--remote-components", "ejs:github"])
185
  cookie_path = _yt_dlp_cookie_file(output_dir)