Spaces:
Sleeping
Sleeping
Fix native upload input and YouTube TLS impersonation
Browse files- app.py +29 -8
- pyproject.toml +1 -0
- 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 |
-
.
|
|
|
|
|
|
|
|
|
|
| 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
|
| 568 |
-
<
|
| 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 |
-
</
|
| 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 => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 665 |
-
|
|
|
|
| 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)
|