kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
/**
* Playwright-based Cloudflare Turnstile solver.
*
* Strategy:
* 1. Launch headless Chromium with stealth patches
* 2. Navigate to geminigen.ai/video (where the Turnstile widget auto-solves)
* 3. Extract the cf-turnstile-response hidden input value
* 4. Cache it for 4.5 minutes (tokens last ~5 min)
*
* For Vercel deployment: uses @sparticuz/chromium-min which downloads the
* Chromium binary at runtime, keeping the function bundle small.
*/
import { chromium, type Browser, type BrowserContext } from "playwright-core";
// The video-gen app page — this URL triggers the Turnstile widget we need.
// NOTE: non-authed visitors are redirected to /auth/signin which also has
// the same Turnstile sitekey (0x4AAAAAACDBydnKT0zYzh2H), so the token
// is still valid for the API call regardless of which page resolves.
const TARGET_URL = "https://geminigen.ai/app/video-gen";
const TOKEN_TTL_MS = 270_000; // 4.5 minutes
interface TokenCache {
token: string;
expiresAt: number;
}
let _cache: TokenCache | null = null;
/** Inject stealth patches to avoid bot detection */
async function applyStealthPatches(context: BrowserContext): Promise<void> {
await context.addInitScript(() => {
// Spoof navigator.webdriver
Object.defineProperty(navigator, "webdriver", {
get: () => undefined,
});
// Add chrome object (expected by fingerprint checks)
(window as any).chrome = {
runtime: {
onConnect: null,
onMessage: null,
},
};
// Spoof permissions API
const originalQuery = window.navigator.permissions.query.bind(
window.navigator.permissions
);
(window.navigator.permissions as any).query = (params: any) => {
if (params.name === "notifications") {
return Promise.resolve({ state: Notification.permission });
}
return originalQuery(params);
};
// Spoof plugin array (headless has none)
Object.defineProperty(navigator, "plugins", {
get: () => [
{ name: "Chrome PDF Plugin" },
{ name: "Chrome PDF Viewer" },
{ name: "Native Client" },
],
});
// Spoof languages
Object.defineProperty(navigator, "languages", {
get: () => ["zh-TW", "zh", "en-US", "en"],
});
// Canvas fingerprint randomization
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (type?: string) {
const dataURL = origToDataURL.call(this, type);
return dataURL;
};
});
}
/** Wait for the Turnstile widget to complete and return the token */
async function waitForTurnstileToken(
context: BrowserContext,
timeoutMs = 30_000
): Promise<string | null> {
const page = await context.newPage();
try {
// Block heavy resources to speed up load
await page.route(
"**/*.{png,jpg,jpeg,gif,webp,svg,ico,woff,woff2,ttf,otf,mp4,mp3,wav}",
(route) => route.abort()
);
await page.goto(TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30_000,
});
// Wait for cf-turnstile-response to be populated
// Turnstile widget injects a hidden <input name="cf-turnstile-response">
const token = await page
.waitForFunction(
() => {
// Standard Turnstile hidden input
const standard = document.querySelector<HTMLInputElement>(
'input[name="cf-turnstile-response"]'
);
if (standard?.value && standard.value.length > 20) return standard.value;
// Some implementations use textarea
const ta = document.querySelector<HTMLTextAreaElement>(
'textarea[name="cf-turnstile-response"]'
);
if (ta?.value && ta.value.length > 20) return ta.value;
// Check for window-level token set by custom callbacks
const win = window as any;
if (win.__cfTurnstileToken && win.__cfTurnstileToken.length > 20) {
return win.__cfTurnstileToken;
}
return null;
},
{ timeout: timeoutMs, polling: 500 }
)
.then((h) => h.jsonValue() as Promise<string>)
.catch(() => null);
return token;
} finally {
await page.close().catch(() => {});
}
}
export interface SolveResult {
token: string;
cached: boolean;
solvedInMs?: number;
}
export async function solveTurnstile(): Promise<SolveResult> {
// Return cached token if still valid
if (_cache && _cache.expiresAt > Date.now()) {
return { token: _cache.token, cached: true };
}
const start = Date.now();
let browser: Browser | null = null;
try {
// Resolve Chromium executable path
// In Vercel Lambda: uses @sparticuz/chromium-min (downloads binary at runtime)
// In local dev: uses system Playwright Chromium
let executablePath: string;
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.VERCEL) {
// Production (Vercel / Lambda): use sparticuz chromium
const chromiumMin = await import("@sparticuz/chromium-min");
const chromiumPack =
process.env.CHROMIUM_PACK_URL ||
"https://github.com/Sparticuz/chromium/releases/download/v133.0.0/chromium-v133.0.0-pack.tar";
executablePath = await chromiumMin.default.executablePath(chromiumPack);
browser = await chromium.launch({
args: chromiumMin.default.args,
executablePath,
headless: true,
});
} else {
// Local dev: use system Playwright Chromium
browser = await chromium.launch({ headless: true });
}
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
viewport: { width: 1280, height: 800 },
locale: "zh-TW",
timezoneId: "Asia/Taipei",
});
await applyStealthPatches(context);
const token = await waitForTurnstileToken(context, 25_000);
await context.close().catch(() => {});
if (!token) {
throw new Error("Turnstile token not found in page DOM after 25s");
}
_cache = { token, expiresAt: Date.now() + TOKEN_TTL_MS };
return { token, cached: false, solvedInMs: Date.now() - start };
} finally {
if (browser) await browser.close().catch(() => {});
}
}
/** Force-invalidate cached token */
export function invalidateCache(): void {
_cache = null;
}