Cleanup: break up the massive app.py into separate modules of manageable size.
This commit is contained in:
+27
-19
@@ -91,22 +91,22 @@ CRAWLER_PROFILES: list[CrawlerProfile] = [
|
||||
CrawlerProfile(
|
||||
"googlebot",
|
||||
"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/128.0.0.0 Safari/537.36",
|
||||
"66.249.64.66", # US, Google
|
||||
"66.249.64.66", # US, Google
|
||||
),
|
||||
CrawlerProfile(
|
||||
"bingbot",
|
||||
"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/128.0.0.0 Safari/537.36",
|
||||
"40.77.167.0", # US, Microsoft
|
||||
"40.77.167.0", # US, Microsoft
|
||||
),
|
||||
CrawlerProfile(
|
||||
"duckduckbot",
|
||||
"DuckDuckBot/1.1; (+http://duckduckgo.com/duckduckbot.html)",
|
||||
"95.217.0.1", # Germany, Hetzner VPS
|
||||
"95.217.0.1", # Germany, Hetzner VPS
|
||||
),
|
||||
CrawlerProfile(
|
||||
"curl",
|
||||
"curl/8.5.0",
|
||||
"139.162.0.1", # Singapore, Linode VPS
|
||||
"139.162.0.1", # Singapore, Linode VPS
|
||||
),
|
||||
]
|
||||
|
||||
@@ -115,14 +115,14 @@ CRAWLER_PROFILES: list[CrawlerProfile] = [
|
||||
# host part; the host may rotate once mid-session.
|
||||
RESIDENTIAL_SOURCE_IPS: list[str] = [
|
||||
# Residential IPv4
|
||||
"91.154.140.209", # Finland, Elisa
|
||||
"84.143.145.207", # Germany, Deutsche Telekom
|
||||
"220.165.255.254", # China, Chinanet / China Telecom
|
||||
"84.235.83.162", # Saudi Arabia, SaudiNet / STC
|
||||
"91.154.140.209", # Finland, Elisa
|
||||
"84.143.145.207", # Germany, Deutsche Telekom
|
||||
"220.165.255.254", # China, Chinanet / China Telecom
|
||||
"84.235.83.162", # Saudi Arabia, SaudiNet / STC
|
||||
# Residential IPv6 /64 prefixes
|
||||
"2a02:8109:ac82:6f0c::/64", # Germany, Deutsche Telekom
|
||||
"240e:45d:1e60:5b0::/64", # China, China Telecom
|
||||
"2409:8904:6720:4123::/64", # China, China Unicom
|
||||
"2a02:8109:ac82:6f0c::/64", # Germany, Deutsche Telekom
|
||||
"240e:45d:1e60:5b0::/64", # China, China Telecom
|
||||
"2409:8904:6720:4123::/64", # China, China Unicom
|
||||
]
|
||||
|
||||
# Concrete datacenter IPs used for abuse scanner bursts. They stay pinned for
|
||||
@@ -130,9 +130,9 @@ RESIDENTIAL_SOURCE_IPS: list[str] = [
|
||||
# Index 0 randomises its UA per request, index 1 uses a fixed browser UA,
|
||||
# and index 2 uses a fixed crawler UA.
|
||||
ABUSE_SOURCE_IPS: list[str] = [
|
||||
"45.63.0.12", # US, Vultr VPS
|
||||
"138.197.0.89", # US, DigitalOcean / Cloudways
|
||||
"2a01:4f8:0:2::1234", # Germany, Hetzner VPS
|
||||
"45.63.0.12", # US, Vultr VPS
|
||||
"138.197.0.89", # US, DigitalOcean / Cloudways
|
||||
"2a01:4f8:0:2::1234", # Germany, Hetzner VPS
|
||||
]
|
||||
|
||||
# Paths commonly probed by attackers looking for exposed config, admin panels,
|
||||
@@ -284,10 +284,16 @@ TAGGED_REFERRERS: list[tuple[str, dict[str, str]]] = [
|
||||
("https://twitter.com/", {"utm_source": "twitter", "utm_medium": "social"}),
|
||||
("https://www.linkedin.com/", {"utm_source": "linkedin", "utm_medium": "social"}),
|
||||
("https://github.com/", {"utm_source": "github", "utm_medium": "referral"}),
|
||||
("https://news.ycombinator.com/", {"utm_source": "hackernews", "utm_medium": "referral"}),
|
||||
(
|
||||
"https://news.ycombinator.com/",
|
||||
{"utm_source": "hackernews", "utm_medium": "referral"},
|
||||
),
|
||||
("https://www.reddit.com/", {"utm_source": "reddit", "utm_medium": "social"}),
|
||||
("https://medium.com/", {"utm_source": "medium", "utm_medium": "referral"}),
|
||||
("https://www.producthunt.com/", {"utm_source": "producthunt", "utm_medium": "referral"}),
|
||||
(
|
||||
"https://www.producthunt.com/",
|
||||
{"utm_source": "producthunt", "utm_medium": "referral"},
|
||||
),
|
||||
]
|
||||
|
||||
# Fraction of referered sessions that also carry UTM tags.
|
||||
@@ -437,7 +443,7 @@ def _random_ipv6_host(prefix: str) -> str:
|
||||
raise ValueError(f"only /64 IPv6 prefixes are supported, got {prefix!r}")
|
||||
if base.endswith("::"):
|
||||
base = base[:-2]
|
||||
host = ":".join(f"{random.randint(0, 0xffff):04x}" for _ in range(4))
|
||||
host = ":".join(f"{random.randint(0, 0xFFFF):04x}" for _ in range(4))
|
||||
return f"{base}:{host}"
|
||||
|
||||
|
||||
@@ -760,9 +766,11 @@ def _run_abuse_scanner(base: str, ip_index: int) -> dict[str, Any]:
|
||||
if ua_mode == 0:
|
||||
get_ua = _abuse_ua
|
||||
elif ua_mode == 1:
|
||||
|
||||
def get_ua() -> str:
|
||||
return BROWSER_PROFILES[0].user_agent
|
||||
else:
|
||||
|
||||
def get_ua() -> str:
|
||||
return CRAWLER_PROFILES[0].user_agent
|
||||
|
||||
@@ -804,8 +812,8 @@ def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace:
|
||||
nargs="?",
|
||||
default="http://localhost:8200",
|
||||
help="Base URL of the Pagerite site (default: http://localhost:8200). "
|
||||
"A bare :PORT or PORT is treated as http://localhost:PORT; a "
|
||||
"missing scheme defaults to http://.",
|
||||
"A bare :PORT or PORT is treated as http://localhost:PORT; a "
|
||||
"missing scheme defaults to http://.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
|
||||
+90
-31
@@ -50,13 +50,34 @@ SEED_X = "ByteDance-Seed/Seed-X-PPO-7B"
|
||||
|
||||
# Seed-X language tags (appended to the prompt; required by its PPO training)
|
||||
SEED_X_TAGS = {
|
||||
"arabic": "ar", "chinese": "zh", "czech": "cs", "danish": "da",
|
||||
"dutch": "nl", "english": "en", "finnish": "fi", "french": "fr",
|
||||
"german": "de", "greek": "el", "hungarian": "hu", "indonesian": "id",
|
||||
"italian": "it", "japanese": "ja", "korean": "ko", "malay": "ms",
|
||||
"norwegian": "no", "persian": "fa", "polish": "pl", "portuguese": "pt",
|
||||
"romanian": "ro", "russian": "ru", "spanish": "es", "swedish": "sv",
|
||||
"thai": "th", "turkish": "tr", "ukrainian": "uk", "vietnamese": "vi",
|
||||
"arabic": "ar",
|
||||
"chinese": "zh",
|
||||
"czech": "cs",
|
||||
"danish": "da",
|
||||
"dutch": "nl",
|
||||
"english": "en",
|
||||
"finnish": "fi",
|
||||
"french": "fr",
|
||||
"german": "de",
|
||||
"greek": "el",
|
||||
"hungarian": "hu",
|
||||
"indonesian": "id",
|
||||
"italian": "it",
|
||||
"japanese": "ja",
|
||||
"korean": "ko",
|
||||
"malay": "ms",
|
||||
"norwegian": "no",
|
||||
"persian": "fa",
|
||||
"polish": "pl",
|
||||
"portuguese": "pt",
|
||||
"romanian": "ro",
|
||||
"russian": "ru",
|
||||
"spanish": "es",
|
||||
"swedish": "sv",
|
||||
"thai": "th",
|
||||
"turkish": "tr",
|
||||
"ukrainian": "uk",
|
||||
"vietnamese": "vi",
|
||||
}
|
||||
SEED_X_NAMES = {v: k for k, v in SEED_X_TAGS.items()}
|
||||
|
||||
@@ -91,11 +112,11 @@ PROMPTS = {
|
||||
# separator in the output (the model merged them) → seed_x_chunk falls
|
||||
# back to the plain kind template.
|
||||
"title+context": "Translate the following {source_lang} title and the beginning of its article "
|
||||
"into {target_lang}:\n{text}\n\n{context} <{tag}>",
|
||||
"into {target_lang}:\n{text}\n\n{context} <{tag}>",
|
||||
# A segment carved out of a larger block (link text, partial run) with
|
||||
# its sentence as context — same mechanics as title+context.
|
||||
"chunk+context": "Translate the following {source_lang} text into {target_lang}:\n"
|
||||
"{text}\n\n{context} <{tag}>",
|
||||
"{text}\n\n{context} <{tag}>",
|
||||
}
|
||||
TERMINAL_PUNCT = ".,!?:;…。,!?;:、"
|
||||
|
||||
@@ -176,7 +197,8 @@ class SeedX:
|
||||
def _load(self):
|
||||
t0 = time.monotonic()
|
||||
self.model = AutoModelForCausalLM.from_pretrained(
|
||||
SEED_X, dtype=torch.bfloat16, device_map="auto")
|
||||
SEED_X, dtype=torch.bfloat16, device_map="auto"
|
||||
)
|
||||
print(f"[seed-x loaded in {time.monotonic() - t0:.0f}s]", file=sys.stderr)
|
||||
|
||||
def get(self):
|
||||
@@ -207,8 +229,16 @@ class SeedX:
|
||||
print(f"[seed-x unloaded after {IDLE_UNLOAD_S}s idle]", file=sys.stderr)
|
||||
|
||||
|
||||
def seed_x_chunk(tokenizer, model, text: str, target_lang: str, tag: str,
|
||||
kind: str = "chunk", context: str = "", source_lang: str = "English"):
|
||||
def seed_x_chunk(
|
||||
tokenizer,
|
||||
model,
|
||||
text: str,
|
||||
target_lang: str,
|
||||
tag: str,
|
||||
kind: str = "chunk",
|
||||
context: str = "",
|
||||
source_lang: str = "English",
|
||||
):
|
||||
"""Translate one segment; returns (translation, output_tokens, generation_seconds).
|
||||
|
||||
With context, the segment is translated together with its surround (a
|
||||
@@ -224,8 +254,13 @@ def seed_x_chunk(tokenizer, model, text: str, target_lang: str, tag: str,
|
||||
"""
|
||||
# No chat template on this model; the trailing language tag is required (trans/ style prompt).
|
||||
template = PROMPTS.get(f"{kind}+context" if context else kind, PROMPTS["chunk"])
|
||||
prompt = template.format(source_lang=source_lang, target_lang=target_lang,
|
||||
text=text, tag=tag, context=context)
|
||||
prompt = template.format(
|
||||
source_lang=source_lang,
|
||||
target_lang=target_lang,
|
||||
text=text,
|
||||
tag=tag,
|
||||
context=context,
|
||||
)
|
||||
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
||||
t0 = time.monotonic()
|
||||
# The only stop string is the context separator. "<" must NOT be one:
|
||||
@@ -234,11 +269,17 @@ def seed_x_chunk(tokenizer, model, text: str, target_lang: str, tag: str,
|
||||
# decode; the post-decode cut at the first "<" then enforces the wire
|
||||
# invariant (prose only) against markup bleed.
|
||||
kwargs = {"stop_strings": ["\n\n"], "tokenizer": tokenizer} if context else {}
|
||||
out = model.generate(**inputs, max_new_tokens=max(1024, 2 * inputs.input_ids.shape[1]),
|
||||
do_sample=False, **kwargs)
|
||||
out = model.generate(
|
||||
**inputs,
|
||||
max_new_tokens=max(1024, 2 * inputs.input_ids.shape[1]),
|
||||
do_sample=False,
|
||||
**kwargs,
|
||||
)
|
||||
dt = time.monotonic() - t0
|
||||
n = out.shape[1] - inputs.input_ids.shape[1]
|
||||
decoded = tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
||||
decoded = tokenizer.decode(
|
||||
out[0][inputs.input_ids.shape[1] :], skip_special_tokens=True
|
||||
)
|
||||
translated = decoded.partition("<")[0]
|
||||
if not context:
|
||||
return translated.strip(), n, dt
|
||||
@@ -256,8 +297,9 @@ def seed_x_chunk(tokenizer, model, text: str, target_lang: str, tag: str,
|
||||
return out, n, dt
|
||||
# The model merged segment and context (no separator, or an empty first
|
||||
# part): retry without the context.
|
||||
again, n2, dt2 = seed_x_chunk(tokenizer, model, text, target_lang, tag,
|
||||
kind=kind, source_lang=source_lang)
|
||||
again, n2, dt2 = seed_x_chunk(
|
||||
tokenizer, model, text, target_lang, tag, kind=kind, source_lang=source_lang
|
||||
)
|
||||
return again, n + n2, dt + dt2
|
||||
|
||||
|
||||
@@ -272,14 +314,20 @@ async def do_job(ws, job: Job, seed_x: SeedX) -> None:
|
||||
tokens = dt = 0
|
||||
for i, text in enumerate(job.texts):
|
||||
ctx = job.contexts[i] if i < len(job.contexts) else ""
|
||||
translated, n, t = seed_x_chunk(tokenizer, model, text, lang_name, job.lang,
|
||||
kind=job.kind, context=ctx)
|
||||
translated, n, t = seed_x_chunk(
|
||||
tokenizer, model, text, lang_name, job.lang, kind=job.kind, context=ctx
|
||||
)
|
||||
texts.append(match_punctuation(text, translated))
|
||||
tokens += n
|
||||
dt += t
|
||||
print(f"[{job.lang} {job.kind} {job.path or '/'}: {len(texts)} segments, "
|
||||
f"{tokens} tokens in {dt:.1f}s = {tokens / dt:.1f} tok/s]", file=sys.stderr)
|
||||
await ws.send(msgspec.json.encode(Result(lang=job.lang, key=job.key, texts=texts)).decode())
|
||||
print(
|
||||
f"[{job.lang} {job.kind} {job.path or '/'}: {len(texts)} segments, "
|
||||
f"{tokens} tokens in {dt:.1f}s = {tokens / dt:.1f} tok/s]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
await ws.send(
|
||||
msgspec.json.encode(Result(lang=job.lang, key=job.key, texts=texts)).decode()
|
||||
)
|
||||
seed_x.idle()
|
||||
|
||||
|
||||
@@ -291,23 +339,34 @@ async def serve(url: str, seed_x: SeedX) -> None:
|
||||
try:
|
||||
async with websockets.connect(url) as ws:
|
||||
backoff = 1
|
||||
await ws.send(msgspec.json.encode(Hello(langs=sorted(SEED_X_NAMES))).decode())
|
||||
print(f"[connected; announced {len(SEED_X_NAMES)} language capabilities]",
|
||||
file=sys.stderr)
|
||||
await ws.send(
|
||||
msgspec.json.encode(Hello(langs=sorted(SEED_X_NAMES))).decode()
|
||||
)
|
||||
print(
|
||||
f"[connected; announced {len(SEED_X_NAMES)} language capabilities]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
async for raw in ws:
|
||||
await do_job(ws, msgspec.json.decode(raw, type=Job), seed_x)
|
||||
except websockets.exceptions.InvalidHandshake:
|
||||
sys.exit("handshake rejected; check the URL (including the key)")
|
||||
except (OSError, websockets.exceptions.ConnectionClosed) as e:
|
||||
print(f"[connection lost ({e}); reconnecting in {backoff}s]", file=sys.stderr)
|
||||
print(
|
||||
f"[connection lost ({e}); reconnecting in {backoff}s]", file=sys.stderr
|
||||
)
|
||||
await asyncio.sleep(backoff)
|
||||
backoff = min(backoff * 2, 60)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("url", help="full translator WebSocket URL including the key, "
|
||||
"e.g. ws://localhost:8410/_translate/KEY")
|
||||
p = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
p.add_argument(
|
||||
"url",
|
||||
help="full translator WebSocket URL including the key, "
|
||||
"e.g. ws://localhost:8410/_translate/KEY",
|
||||
)
|
||||
args = p.parse_args()
|
||||
if not args.url.startswith(("ws://", "wss://")):
|
||||
p.error("url must start with ws:// or wss://")
|
||||
|
||||
Reference in New Issue
Block a user