Spaces:
Running
Running
Upload 2 files
Browse files
app.py
CHANGED
|
@@ -132,18 +132,20 @@ def upsell_html(up):
|
|
| 132 |
icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄"}.get(kind, "✨")
|
| 133 |
title = html.escape(getattr(up, "headline", "") or "Heads up")
|
| 134 |
text = html.escape(getattr(up, "text", "") or str(up))
|
| 135 |
-
st = P.PLANS["
|
| 136 |
perks = {
|
| 137 |
-
"upgrade": [f"
|
| 138 |
-
f"
|
| 139 |
-
"
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
"
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
| 145 |
}.get(kind, [])
|
| 146 |
-
go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy
|
| 147 |
"signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
|
| 148 |
perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
|
| 149 |
btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
|
|
@@ -240,6 +242,8 @@ def _stable_uploads(args):
|
|
| 240 |
def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool="", words=None, paste=None,
|
| 241 |
quota=True):
|
| 242 |
args = _stable_uploads(args)
|
|
|
|
|
|
|
| 243 |
key = _job_key(user, request, tool, args)
|
| 244 |
job = H.find_job(key)
|
| 245 |
if job is not None and job.done.is_set() and (job.delivered or "error" in job.box):
|
|
@@ -251,7 +255,7 @@ def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool=
|
|
| 251 |
if need_login:
|
| 252 |
A._require(user)
|
| 253 |
if quota:
|
| 254 |
-
P.check_doc(user, words)
|
| 255 |
if quota:
|
| 256 |
A.check_limit(user, request, need_login)
|
| 257 |
except A.Upsell as up: # plan / sign-in / limit -> friendly pop-up, not an error
|
|
@@ -271,7 +275,7 @@ def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool=
|
|
| 271 |
if quota:
|
| 272 |
A.add_usage(user, request, need_login)
|
| 273 |
if need_login and quota:
|
| 274 |
-
P.consume_doc(user)
|
| 275 |
j.extra["mailed"] = M.send_result(user, tool, meta)
|
| 276 |
b_, a_ = meta.get("before"), meta.get("after")
|
| 277 |
score = f" — AI score {b_}% → {a_}%" if (b_ is not None and a_ is not None) else ""
|
|
@@ -338,6 +342,16 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
|
|
| 338 |
# customers see only the headline; the pass log and engine notes go to the Space logs
|
| 339 |
print(f"[smart] {name}: AI {b['pct']}% -> {a['pct']}% | " + " | ".join(res["log"]) + f" |{res['note']}", flush=True)
|
| 340 |
msg = f"✅ **Done** — AI score {b['pct']}% → {a['pct']}%."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
labels, g_before, g_after = None, b["pct"], a["pct"]
|
| 342 |
gline = _grammar_line(res.get("note"))
|
| 343 |
rs = res.get("report")
|
|
@@ -477,7 +491,7 @@ def smart_text(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report
|
|
| 477 |
|
| 478 |
|
| 479 |
def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
|
| 480 |
-
passes = P.max_passes(user, passes)
|
| 481 |
yield from _stream(_smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report, rkind), 5, 0, user, True, request,
|
| 482 |
tool="Smart Humanize", words=P.doc_words(path) if path else None)
|
| 483 |
|
|
@@ -1750,13 +1764,19 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1750 |
_b = P.price_buttons(P.DEFAULT_CURRENCY)
|
| 1751 |
with gr.Row():
|
| 1752 |
pr_single = gr.Button(_b[0])
|
| 1753 |
-
|
|
|
|
|
|
|
| 1754 |
pr_pay = gr.HTML()
|
| 1755 |
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">Prices in Indian rupees (₹) '
|
| 1756 |
-
'for India and US dollars ($) for other countries, switch with Currency above.
|
| 1757 |
-
'
|
| 1758 |
-
|
| 1759 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1760 |
'Results are indicators from our own detector and can differ from other checkers.</div>')
|
| 1761 |
|
| 1762 |
# ============================ ABOUT / CONTACT ============================
|
|
@@ -1890,14 +1910,14 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1890 |
'(refund, misuse, user request); the user goes back to the free plan.</div>')
|
| 1891 |
with gr.Row():
|
| 1892 |
ad_p_email = gr.Textbox(label="User email", scale=3)
|
| 1893 |
-
ad_p_plan = gr.Dropdown(
|
| 1894 |
label="Plan", scale=2)
|
| 1895 |
ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
|
| 1896 |
ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
|
| 1897 |
with gr.Row():
|
| 1898 |
ad_p_activate = gr.Button("✅ Activate plan", variant="primary")
|
| 1899 |
-
ad_p_credits_n = gr.Number(value=1, label="
|
| 1900 |
-
ad_p_credits = gr.Button("➕ Add
|
| 1901 |
with gr.Row():
|
| 1902 |
ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
|
| 1903 |
scale=3)
|
|
@@ -2133,9 +2153,9 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 2133 |
def _switch_currency(c):
|
| 2134 |
b = P.price_buttons(c)
|
| 2135 |
return (P.pricing_html(c),) + tuple(gr.update(value=x) for x in b)
|
| 2136 |
-
pr_cur.change(_switch_currency, pr_cur, [pr_html, pr_single,
|
| 2137 |
|
| 2138 |
-
for b, item in ((pr_single,
|
| 2139 |
b.click(lambda u, item=item: P.pay_instructions(u, item), user_state, pr_pay, **HID)
|
| 2140 |
|
| 2141 |
# admin: plans, credits, requests, payments
|
|
|
|
| 132 |
icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄"}.get(kind, "✨")
|
| 133 |
title = html.escape(getattr(up, "headline", "") or "Heads up")
|
| 134 |
text = html.escape(getattr(up, "text", "") or str(up))
|
| 135 |
+
st = P.PLANS["scholar"]
|
| 136 |
perks = {
|
| 137 |
+
"upgrade": [f"1 credit: {P.price_of('single', 'INR')} / {P.price_of('single', 'USD')} · 5 credits: "
|
| 138 |
+
f"{P.price_of('pack', 'INR')} / {P.price_of('pack', 'USD')}",
|
| 139 |
+
f"Scholar: {st['docs_month']} credits a month for ₹{st['inr']:,} / ${st['usd']:,.0f}",
|
| 140 |
+
f"1 credit = 1 document up to {P.CREDIT_WORDS:,} words",
|
| 141 |
+
"Word layout kept + PDF report + proofread"],
|
| 142 |
+
"signin": [f"Free trial document (up to {P.PLANS['free']['doc_words']:,} words)",
|
| 143 |
+
f"{P.FREE_DETECT_DAY} detector reports a day", "Run history & re-downloads", "No card needed"],
|
| 144 |
+
"extra": [f"Extra credits: {P.price_of('extra', 'INR')} / {P.price_of('extra', 'USD')} each",
|
| 145 |
+
"Continue straight away", f"Bought credits stay valid {P.CREDIT_MONTHS} months",
|
| 146 |
+
"Your monthly credits start again next month"],
|
| 147 |
}.get(kind, [])
|
| 148 |
+
go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy credits", "cs-go-pricing"),
|
| 149 |
"signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
|
| 150 |
perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
|
| 151 |
btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
|
|
|
|
| 242 |
def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool="", words=None, paste=None,
|
| 243 |
quota=True):
|
| 244 |
args = _stable_uploads(args)
|
| 245 |
+
# the uploaded file's content key (folder name under inputs/): the same file re-run = free re-run
|
| 246 |
+
fkey = next((Path(str(a)).parent.name for a in args if isinstance(a, str) and a.startswith(str(_IN_DIR))), None)
|
| 247 |
key = _job_key(user, request, tool, args)
|
| 248 |
job = H.find_job(key)
|
| 249 |
if job is not None and job.done.is_set() and (job.delivered or "error" in job.box):
|
|
|
|
| 255 |
if need_login:
|
| 256 |
A._require(user)
|
| 257 |
if quota:
|
| 258 |
+
P.check_doc(user, words, tool, fkey) # credits / plan / free trial / free re-run + word cap
|
| 259 |
if quota:
|
| 260 |
A.check_limit(user, request, need_login)
|
| 261 |
except A.Upsell as up: # plan / sign-in / limit -> friendly pop-up, not an error
|
|
|
|
| 275 |
if quota:
|
| 276 |
A.add_usage(user, request, need_login)
|
| 277 |
if need_login and quota:
|
| 278 |
+
P.consume_doc(user, words, tool, fkey)
|
| 279 |
j.extra["mailed"] = M.send_result(user, tool, meta)
|
| 280 |
b_, a_ = meta.get("before"), meta.get("after")
|
| 281 |
score = f" — AI score {b_}% → {a_}%" if (b_ is not None and a_ is not None) else ""
|
|
|
|
| 342 |
# customers see only the headline; the pass log and engine notes go to the Space logs
|
| 343 |
print(f"[smart] {name}: AI {b['pct']}% -> {a['pct']}% | " + " | ".join(res["log"]) + f" |{res['note']}", flush=True)
|
| 344 |
msg = f"✅ **Done** — AI score {b['pct']}% → {a['pct']}%."
|
| 345 |
+
log_ = res.get("log") or []
|
| 346 |
+
n_pass = len({m.group(1) for m in (re.match(r"Pass (\d+): AI score .*rewrote", x) for x in log_) if m})
|
| 347 |
+
why = next((x.split(" — ", 1)[1] for x in reversed(log_) if " — " in x), "")
|
| 348 |
+
locked = max([int(m.group(1)) for m in (re.search(r"\((\d+) skipped", x) for x in log_) if m] or [0])
|
| 349 |
+
if n_pass:
|
| 350 |
+
msg += (f" ({n_pass} pass{'es' if n_pass != 1 else ''}"
|
| 351 |
+
+ (f"; {why.replace('stopped: ', 'stopped after ')}" if why else "") + ")")
|
| 352 |
+
if locked:
|
| 353 |
+
msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
|
| 354 |
+
"sub/superscripts inside the sentence) and keep their wording.")
|
| 355 |
labels, g_before, g_after = None, b["pct"], a["pct"]
|
| 356 |
gline = _grammar_line(res.get("note"))
|
| 357 |
rs = res.get("report")
|
|
|
|
| 491 |
|
| 492 |
|
| 493 |
def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
|
| 494 |
+
passes = P.max_passes(user, passes, document=True)
|
| 495 |
yield from _stream(_smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report, rkind), 5, 0, user, True, request,
|
| 496 |
tool="Smart Humanize", words=P.doc_words(path) if path else None)
|
| 497 |
|
|
|
|
| 1764 |
_b = P.price_buttons(P.DEFAULT_CURRENCY)
|
| 1765 |
with gr.Row():
|
| 1766 |
pr_single = gr.Button(_b[0])
|
| 1767 |
+
pr_pack = gr.Button(_b[1])
|
| 1768 |
+
pr_scholar = gr.Button(_b[2], variant="primary")
|
| 1769 |
+
pr_pro = gr.Button(_b[3], variant="primary")
|
| 1770 |
pr_pay = gr.HTML()
|
| 1771 |
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">Prices in Indian rupees (₹) '
|
| 1772 |
+
'for India and US dollars ($) for other countries, switch with Currency above. One credit = one '
|
| 1773 |
+
'document up to ' + f"{P.CREDIT_WORDS:,}" + ' words (longer files use one credit per started '
|
| 1774 |
+
+ f"{P.CREDIT_WORDS:,}" + ' words; the count is shown before the run). Bought credits are valid '
|
| 1775 |
+
+ str(P.CREDIT_MONTHS) + ' months. Monthly plans run for one month from the day they start; '
|
| 1776 |
+
'unused monthly credits do not carry over, and a plan not renewed by its end date is switched off '
|
| 1777 |
+
'automatically. A run that fails costs nothing, and the same file can be re-run '
|
| 1778 |
+
+ str(P.FREE_RERUNS) + ' times free within ' + str(P.RERUN_DAYS) + ' days. Refund: a credit is '
|
| 1779 |
+
'refunded if a run did not lower the AI score on our detector and the free re-runs did not either. '
|
| 1780 |
'Results are indicators from our own detector and can differ from other checkers.</div>')
|
| 1781 |
|
| 1782 |
# ============================ ABOUT / CONTACT ============================
|
|
|
|
| 1910 |
'(refund, misuse, user request); the user goes back to the free plan.</div>')
|
| 1911 |
with gr.Row():
|
| 1912 |
ad_p_email = gr.Textbox(label="User email", scale=3)
|
| 1913 |
+
ad_p_plan = gr.Dropdown(list(P.ADMIN_PLANS), value="scholar",
|
| 1914 |
label="Plan", scale=2)
|
| 1915 |
ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
|
| 1916 |
ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
|
| 1917 |
with gr.Row():
|
| 1918 |
ad_p_activate = gr.Button("✅ Activate plan", variant="primary")
|
| 1919 |
+
ad_p_credits_n = gr.Number(value=1, label="Credits (1 single, 5 pack)", precision=0)
|
| 1920 |
+
ad_p_credits = gr.Button("➕ Add credits")
|
| 1921 |
with gr.Row():
|
| 1922 |
ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
|
| 1923 |
scale=3)
|
|
|
|
| 2153 |
def _switch_currency(c):
|
| 2154 |
b = P.price_buttons(c)
|
| 2155 |
return (P.pricing_html(c),) + tuple(gr.update(value=x) for x in b)
|
| 2156 |
+
pr_cur.change(_switch_currency, pr_cur, [pr_html, pr_single, pr_pack, pr_scholar, pr_pro], queue=False, **HID)
|
| 2157 |
|
| 2158 |
+
for b, item in zip((pr_single, pr_pack, pr_scholar, pr_pro), P.BUY_ITEMS):
|
| 2159 |
b.click(lambda u, item=item: P.pay_instructions(u, item), user_state, pr_pay, **HID)
|
| 2160 |
|
| 2161 |
# admin: plans, credits, requests, payments
|
plans.py
CHANGED
|
@@ -1,28 +1,29 @@
|
|
| 1 |
"""
|
| 2 |
-
Pricing plans,
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
"""
|
| 24 |
import calendar
|
| 25 |
import html
|
|
|
|
| 26 |
import os
|
| 27 |
import threading
|
| 28 |
import time
|
|
@@ -31,34 +32,65 @@ import gradio as gr
|
|
| 31 |
|
| 32 |
import auth as A
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
PAY_EMAIL = os.getenv("PAY_EMAIL", "info@cssoftwaresolutions.tech")
|
| 36 |
-
PAY_WHATSAPP = os.getenv("PAY_WHATSAPP", "918886306793")
|
| 37 |
PROMO_TEXT = os.getenv("PROMO_TEXT", "")
|
| 38 |
UNLIMITED = 10 ** 9
|
| 39 |
|
| 40 |
-
#
|
| 41 |
PLANS = {
|
| 42 |
-
"guest":
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
docs_day=0, pastes=UNLIMITED, paste_words=
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
-
for _old in ("student", "agency"
|
| 52 |
-
PLANS[_old] = PLANS["
|
| 53 |
-
PAID = ("
|
| 54 |
-
_PAID_SQL = "('
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
DEFAULT_CURRENCY = os.getenv("PRICE_CURRENCY", "INR").strip().upper()
|
|
|
|
| 62 |
|
| 63 |
|
| 64 |
def _cur(currency):
|
|
@@ -67,26 +99,33 @@ def _cur(currency):
|
|
| 67 |
|
| 68 |
|
| 69 |
def money(amount, cur="INR", per=""):
|
| 70 |
-
"""₹
|
| 71 |
if not amount:
|
| 72 |
return "Free"
|
| 73 |
return f"₹{amount:,.0f}{per}" if cur == "INR" else f"${amount:,.0f}{per}"
|
| 74 |
|
| 75 |
|
| 76 |
def price_of(item, cur="INR"):
|
| 77 |
-
p =
|
| 78 |
return money(p["usd"] if cur == "USD" else p["inr"], cur)
|
| 79 |
|
| 80 |
|
| 81 |
def both(item):
|
| 82 |
-
"""'₹
|
| 83 |
c1 = _cur(DEFAULT_CURRENCY)
|
| 84 |
c2 = "USD" if c1 == "INR" else "INR"
|
| 85 |
return f"{price_of(item, c1)} ({price_of(item, c2)})"
|
| 86 |
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
def per_day_text(p):
|
| 89 |
-
return f"{p['docs_month']}
|
|
|
|
|
|
|
| 90 |
# ---------------------------------------------------------------- storage
|
| 91 |
def _db():
|
| 92 |
con = A._db()
|
|
@@ -94,7 +133,7 @@ def _db():
|
|
| 94 |
for c, ddl in (("plan", "TEXT DEFAULT 'free'"), ("plan_until", "TEXT"), ("period_start", "TEXT"),
|
| 95 |
("period_docs", "INTEGER DEFAULT 0"), ("doc_credits", "INTEGER DEFAULT 0"),
|
| 96 |
("trial_used", "INTEGER DEFAULT 0"), ("plan_started", "TEXT"),
|
| 97 |
-
("plan_status", "TEXT DEFAULT 'free'")):
|
| 98 |
if c not in cols:
|
| 99 |
try:
|
| 100 |
con.execute(f"ALTER TABLE users ADD COLUMN {c} {ddl}")
|
|
@@ -108,6 +147,10 @@ def _db():
|
|
| 108 |
con.execute("CREATE TABLE IF NOT EXISTS plan_history (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
| 109 |
"ts TEXT DEFAULT CURRENT_TIMESTAMP, email TEXT, plan TEXT, started TEXT, until TEXT, "
|
| 110 |
"event TEXT, note TEXT)")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
return con
|
| 112 |
|
| 113 |
|
|
@@ -192,14 +235,24 @@ def state(user):
|
|
| 192 |
"""Everything the UI and the checks need about a user's plan."""
|
| 193 |
if A.is_admin(user):
|
| 194 |
return dict(key="admin", **PLANS["admin"], until=None, started=None, used=0, used_today=0, credits=0,
|
|
|
|
| 195 |
trial_left=0, paid=True, admin=True, status="admin", month_end=None)
|
| 196 |
if not user:
|
| 197 |
return dict(key="guest", **PLANS["guest"], until=None, started=None, used=0, used_today=0, credits=0,
|
|
|
|
| 198 |
trial_left=0, paid=False, admin=False, status="guest", month_end=None)
|
| 199 |
with _db() as con:
|
| 200 |
row = con.execute("SELECT plan, plan_until, plan_started, doc_credits, trial_used, plan_status, "
|
| 201 |
-
"period_start, period_docs FROM users WHERE email=?",
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
if plan in PAID and until and _parse(until) <= _now():
|
| 204 |
expire_due(user["email"]) # month over, not renewed: switch it off now
|
| 205 |
plan, status = "free", "expired"
|
|
@@ -214,13 +267,14 @@ def state(user):
|
|
| 214 |
p = PLANS.get(plan, PLANS["free"])
|
| 215 |
return dict(key=plan if plan in PLANS else "free", **p, until=until if plan in PAID else None,
|
| 216 |
started=started if plan in PAID else None, used=used, used_today=used, credits=credits or 0,
|
|
|
|
| 217 |
trial_left=max(0, FREE_TRIAL_DOCS - (trial or 0)), month_end=month_end,
|
| 218 |
paid=plan in PAID or (credits or 0) > 0, admin=False,
|
| 219 |
status="active" if plan in PAID else (status or "free"))
|
| 220 |
|
| 221 |
|
| 222 |
def docs_available(st):
|
| 223 |
-
"""
|
| 224 |
if st["key"] in PAID:
|
| 225 |
return max(0, st["docs_month"] - st["used"]) + st["credits"]
|
| 226 |
return st["credits"] + st["trial_left"]
|
|
@@ -231,31 +285,93 @@ def _upgrade_msg(reason):
|
|
| 231 |
return f"{reason} See the 💎 Pricing tab to upgrade."
|
| 232 |
|
| 233 |
|
| 234 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
st = state(user)
|
| 236 |
if st["admin"]:
|
| 237 |
return
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
raise A.Upsell(
|
| 254 |
-
"
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
|
| 261 |
def paste_limit(user):
|
|
@@ -266,10 +382,10 @@ def check_paste(user, text):
|
|
| 266 |
st = state(user)
|
| 267 |
n = len((text or "").split())
|
| 268 |
if n > st["paste_words"]:
|
| 269 |
-
if st["key"] in
|
| 270 |
-
raise A.Upsell(f"Free pastes are up to {st['paste_words']} words (yours: {n:,}).
|
| 271 |
-
f"{PLANS['
|
| 272 |
-
title="Your text is longer than the free limit")
|
| 273 |
raise A.Upsell(f"Your plan allows {st['paste_words']:,} words per paste (yours: {n:,}). Upload a .docx for "
|
| 274 |
"longer text.", kind="limit", title="Your text is a bit long")
|
| 275 |
|
|
@@ -278,12 +394,17 @@ def daily_paste_limit(user):
|
|
| 278 |
return state(user)["pastes"]
|
| 279 |
|
| 280 |
|
| 281 |
-
def max_passes(user, requested):
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
|
| 285 |
def doc_words(path):
|
| 286 |
-
"""Quick word count of an uploaded file (for the
|
| 287 |
try:
|
| 288 |
p = str(path).lower()
|
| 289 |
if p.endswith(".docx"):
|
|
@@ -306,19 +427,37 @@ def doc_words(path):
|
|
| 306 |
|
| 307 |
|
| 308 |
# ---------------------------------------------------------------- consume (after a successful run)
|
| 309 |
-
def consume_doc(user):
|
| 310 |
-
"""
|
|
|
|
| 311 |
st = state(user)
|
| 312 |
if st["admin"] or not user:
|
| 313 |
return
|
| 314 |
e = user["email"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
with _db() as con:
|
| 316 |
-
if
|
| 317 |
-
con.execute("
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
| 321 |
con.execute("UPDATE users SET trial_used=COALESCE(trial_used,0)+1 WHERE email=?", (e,))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
A.request_sync()
|
| 323 |
|
| 324 |
|
|
@@ -399,17 +538,22 @@ def terminate_plan(email, reason="", notify=True, source="admin"):
|
|
| 399 |
|
| 400 |
|
| 401 |
def add_credits(email, n, source="admin", amount="", note=""):
|
|
|
|
|
|
|
| 402 |
email = (email or "").strip().lower()
|
|
|
|
|
|
|
|
|
|
| 403 |
with _db() as con:
|
| 404 |
-
k = con.execute("UPDATE users SET doc_credits=COALESCE(doc_credits,0)+? WHERE email=?",
|
| 405 |
-
(int(n), email)).rowcount
|
| 406 |
if not k:
|
| 407 |
return False, f"No user found with {email}."
|
| 408 |
con.execute("INSERT INTO payments (email, item, amount, source, note) VALUES (?,?,?,?,?)",
|
| 409 |
-
(email, f"{int(n)}
|
| 410 |
con.execute("UPDATE upgrade_requests SET status='done' WHERE email=? AND status='open'", (email,))
|
| 411 |
A.request_sync()
|
| 412 |
-
return True, f"Added {int(n)}
|
| 413 |
|
| 414 |
|
| 415 |
def request_upgrade(user, item):
|
|
@@ -438,43 +582,41 @@ def usage_line(user):
|
|
| 438 |
if st["admin"]:
|
| 439 |
return "Admin — everything is free and unlimited."
|
| 440 |
if not user:
|
| 441 |
-
return (f"Guest: {st['paste_words']}-word pastes, {st['pastes']} runs a day. "
|
| 442 |
-
f"
|
| 443 |
parts = []
|
| 444 |
if st["key"] in PAID:
|
| 445 |
-
parts.append(f"
|
| 446 |
-
f"(new month starts {_ist_date(st['month_end'])}) · paid until {_ist_date(st['until'])}, "
|
| 447 |
-
|
| 448 |
elif st["status"] == "expired":
|
| 449 |
parts.append("Your plan ended (not renewed). Renew on the Pricing tab")
|
| 450 |
elif st["status"] == "terminated":
|
| 451 |
parts.append("Your subscription has been ended. Contact us or choose an option on the Pricing tab")
|
| 452 |
else:
|
| 453 |
-
parts.append(f"Free plan: {st['trial_left']} of {FREE_TRIAL_DOCS}
|
| 454 |
if st["credits"]:
|
| 455 |
-
parts.append(f"{st['credits']}
|
|
|
|
| 456 |
return " · ".join(parts)
|
| 457 |
|
| 458 |
|
| 459 |
def nudge(user, document):
|
| 460 |
"""Shown under a finished run for guests and free users."""
|
| 461 |
st = state(user)
|
| 462 |
-
if st["admin"] or st["key"] in PAID:
|
| 463 |
return ""
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
f"{both('monthly')}/month.")
|
| 467 |
if not user:
|
| 468 |
-
msg = (f"Want to check a whole
|
| 469 |
-
f"layout kept, highlighted PDF report included.")
|
| 470 |
elif st["status"] in ("expired", "terminated"):
|
| 471 |
msg = f"<b>Your plan has ended.</b> {offer}"
|
| 472 |
-
elif st["credits"] > 0:
|
| 473 |
-
return ""
|
| 474 |
elif st["trial_left"] > 0:
|
| 475 |
-
msg = f"<b>
|
| 476 |
else:
|
| 477 |
-
msg = f"<b>Your free
|
| 478 |
return f'<div class="cs-nudge">💎 {msg} Open the <b>Pricing</b> tab.</div>'
|
| 479 |
|
| 480 |
|
|
@@ -482,60 +624,79 @@ def pricing_html(currency=None):
|
|
| 482 |
cur = _cur(currency)
|
| 483 |
other = "USD" if cur == "INR" else "INR"
|
| 484 |
where = "outside India" if other == "USD" else "in India"
|
| 485 |
-
|
| 486 |
-
common = ["Smart Humanize + AI
|
| 487 |
-
"
|
| 488 |
|
| 489 |
-
def card(title, price, per, alt, feats, featured=False):
|
| 490 |
li = "".join(f"<li>{html.escape(f)}</li>" for f in feats)
|
| 491 |
-
tag = '<div class="cs-pop">
|
| 492 |
return (f'<div class="cs-price{" cs-featured" if featured else ""}">{tag}<div class="cs-pname">'
|
| 493 |
f'{html.escape(title)}</div><div class="cs-pval">{price}<small>{per}</small></div>'
|
| 494 |
f'<div class="cs-inr">{alt} {where}</div><ul>{li}</ul></div>')
|
| 495 |
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
promo = f'<div class="cs-promo">🎉 {html.escape(PROMO_TEXT)}</div>' if PROMO_TEXT else ""
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
|
| 511 |
def price_buttons(currency=None):
|
| 512 |
-
"""Labels of the
|
| 513 |
cur = _cur(currency)
|
| 514 |
-
return (f"Buy 1
|
|
|
|
| 515 |
|
| 516 |
|
| 517 |
def pay_instructions(user, item):
|
| 518 |
-
"""Until
|
| 519 |
if A.is_admin(user):
|
| 520 |
return '<div class="cs-authmsg cs-ok">You are an admin — every feature is free for you.</div>'
|
| 521 |
if not user:
|
| 522 |
return ('<div class="cs-authmsg cs-bad">Please sign in (or create a free account) first, so we can '
|
| 523 |
-
'activate
|
| 524 |
request_upgrade(user, item)
|
| 525 |
links = []
|
| 526 |
if PAY_WHATSAPP:
|
| 527 |
-
txt =
|
| 528 |
-
else f"Hi, I want the {item} for {user['email']}").replace(" ", "%20")
|
| 529 |
links.append(f'<a class="cs-dl" target="_blank" href="https://wa.me/{PAY_WHATSAPP}?text={txt}">'
|
| 530 |
f'💬 Pay via WhatsApp / UPI</a>')
|
| 531 |
if PAY_EMAIL:
|
| 532 |
-
links.append(f'<a class="cs-dl" href="mailto:{PAY_EMAIL}?subject=
|
| 533 |
f'&body=Account:%20{user["email"]}">✉️ Email us to pay</a>')
|
| 534 |
contact = "".join(links) or (f'<div class="cs-gauge-cap" style="text-align:left">Contact us through '
|
| 535 |
f'<a href="https://cssoftwaresolutions.in" target="_blank">cssoftwaresolutions.in</a>.</div>')
|
| 536 |
return (f'<div class="cs-authmsg cs-ok"><b>{html.escape(item)}</b> requested for '
|
| 537 |
f'<b>{html.escape(user["email"])}</b>. Online card payment is launching shortly; for now pay by UPI or '
|
| 538 |
-
f'bank transfer and
|
| 539 |
|
| 540 |
|
| 541 |
# ---------------------------------------------------------------- admin
|
|
|
|
| 1 |
"""
|
| 2 |
+
Pricing plans, credits, quotas and upgrade nudges (pricing policy of October 2026).
|
| 3 |
+
|
| 4 |
+
Unit: one CREDIT = one document of up to CREDIT_WORDS (15,000) words; a longer document uses one credit per started
|
| 5 |
+
15,000 words (a 40,000-word thesis = 3 credits). The number is shown before the run and charged only after a
|
| 6 |
+
successful run (a failed run costs nothing). The same file can be re-run FREE_RERUNS times free within RERUN_DAYS.
|
| 7 |
+
|
| 8 |
+
guest paste only: 300 words a paste, 15 a day, 2 Smart passes
|
| 9 |
+
free 25 pastes a day (300 words); 1 trial document up to 5,000 words; 3 detector files a day
|
| 10 |
+
single ₹199 / $3 one credit, valid 6 months
|
| 11 |
+
pack ₹799 / $12 five credits, valid 6 months
|
| 12 |
+
scholar ₹999 / $14 a month 10 credits a month, pastes up to 1,500 words, unlimited detector
|
| 13 |
+
pro ₹2,999 / $39 a month 40 credits a month, pastes up to 5,000 words, priority
|
| 14 |
+
institution ₹9,999 / $129 a month 150 credits a month (activated by the admin, invoice billing)
|
| 15 |
+
extra ₹149 / $2 one extra credit for monthly-plan users whose month's credits are used
|
| 16 |
+
admin everything free and unlimited
|
| 17 |
+
Journal Finder: free. Journal Formatter: 1 file a month free, included in plans (5 / 20 / 60 a month), ₹99 / $2 per
|
| 18 |
+
file otherwise (shown on the Pricing page; enforced once the Formatter shares the suite's login).
|
| 19 |
+
Monthly plans: 30 days from activation, credits do not carry over, renewal before expiry adds a month on top;
|
| 20 |
+
not renewed by the end date -> switched off automatically (plan_history + email). Prices include GST (INR).
|
| 21 |
+
|
| 22 |
+
Money comes in through `activate_plan()` / `add_credits()` (Admin panel today, Razorpay webhook later).
|
| 23 |
"""
|
| 24 |
import calendar
|
| 25 |
import html
|
| 26 |
+
import math
|
| 27 |
import os
|
| 28 |
import threading
|
| 29 |
import time
|
|
|
|
| 32 |
|
| 33 |
import auth as A
|
| 34 |
|
| 35 |
+
|
| 36 |
+
def _env_i(k, d):
|
| 37 |
+
return int(os.getenv(k, str(d)))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _env_f(k, d):
|
| 41 |
+
return float(os.getenv(k, str(d)))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
FREE_TRIAL_DOCS = _env_i("FREE_TRIAL_DOCS", 1)
|
| 45 |
+
FREE_DETECT_DAY = _env_i("FREE_DETECT_DAY", 3) # detector file reports a day on the free account
|
| 46 |
+
CREDIT_WORDS = _env_i("CREDIT_WORDS", 15000) # words per credit
|
| 47 |
+
CREDIT_MONTHS = _env_i("CREDIT_MONTHS", 6) # bought credits stay valid this many months
|
| 48 |
+
FREE_RERUNS = _env_i("FREE_RERUNS", 2) # free re-runs of the same file ...
|
| 49 |
+
RERUN_DAYS = _env_i("RERUN_DAYS", 7) # ... within this many days
|
| 50 |
PAY_EMAIL = os.getenv("PAY_EMAIL", "info@cssoftwaresolutions.tech")
|
| 51 |
+
PAY_WHATSAPP = os.getenv("PAY_WHATSAPP", "918886306793")
|
| 52 |
PROMO_TEXT = os.getenv("PROMO_TEXT", "")
|
| 53 |
UNLIMITED = 10 ** 9
|
| 54 |
|
| 55 |
+
# docs_month = credits per subscription month; format_month = Journal Formatter files a month (display)
|
| 56 |
PLANS = {
|
| 57 |
+
"guest": dict(name="Guest", inr=0, usd=0, docs_month=0, docs_day=0, pastes=15, paste_words=300, passes=2,
|
| 58 |
+
doc_words=0, format_month=0, priority=False),
|
| 59 |
+
"free": dict(name="Free", inr=0, usd=0, docs_month=0, docs_day=0, pastes=25, paste_words=300, passes=2,
|
| 60 |
+
doc_words=_env_i("FREE_DOC_WORDS", 5000), format_month=1, priority=False),
|
| 61 |
+
"scholar": dict(name="Scholar", inr=_env_i("PRICE_SCHOLAR", 999), usd=_env_f("PRICE_SCHOLAR_USD", 14),
|
| 62 |
+
docs_month=_env_i("SCHOLAR_CREDITS", 10), docs_day=0, pastes=UNLIMITED, paste_words=1500,
|
| 63 |
+
passes=10, doc_words=150000, format_month=5, priority=False),
|
| 64 |
+
"pro": dict(name="Pro", inr=_env_i("PRICE_PRO", 2999), usd=_env_f("PRICE_PRO_USD", 39),
|
| 65 |
+
docs_month=_env_i("PRO_CREDITS", 40), docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=10,
|
| 66 |
+
doc_words=150000, format_month=20, priority=True),
|
| 67 |
+
"institution": dict(name="Institution", inr=_env_i("PRICE_INSTITUTION", 9999),
|
| 68 |
+
usd=_env_f("PRICE_INSTITUTION_USD", 129), docs_month=_env_i("INSTITUTION_CREDITS", 150),
|
| 69 |
+
docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=10, doc_words=150000,
|
| 70 |
+
format_month=60, priority=True),
|
| 71 |
+
# earlier plan (₹4,999 for 300 documents a month): kept so existing subscriptions run to their end date
|
| 72 |
+
"monthly": dict(name="Monthly (earlier plan)", inr=4999, usd=59, docs_month=_env_i("MONTHLY_DOCS", 300),
|
| 73 |
+
docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=10, doc_words=150000, format_month=20,
|
| 74 |
+
priority=True),
|
| 75 |
+
"admin": dict(name="Admin", inr=0, usd=0, docs_month=0, docs_day=0, pastes=UNLIMITED, paste_words=20000,
|
| 76 |
+
passes=10, doc_words=UNLIMITED, format_month=UNLIMITED, priority=True),
|
| 77 |
}
|
| 78 |
+
for _old in ("student", "agency"): # names from an earlier price list
|
| 79 |
+
PLANS[_old] = PLANS["scholar"]
|
| 80 |
+
PAID = ("scholar", "pro", "institution", "monthly", "student", "agency")
|
| 81 |
+
_PAID_SQL = "('scholar','pro','institution','monthly','student','agency')"
|
| 82 |
+
SELL = ("scholar", "pro") # monthly plans on the Pricing page (institution: on request)
|
| 83 |
+
ADMIN_PLANS = ("scholar", "pro", "institution", "monthly")
|
| 84 |
+
|
| 85 |
+
# credit products
|
| 86 |
+
SINGLE = dict(name="Single credit", inr=_env_i("PRICE_SINGLE", 199), usd=_env_f("PRICE_SINGLE_USD", 3), credits=1)
|
| 87 |
+
PACK = dict(name="Pack of 5 credits", inr=_env_i("PRICE_PACK", 799), usd=_env_f("PRICE_PACK_USD", 12), credits=5)
|
| 88 |
+
EXTRA = dict(name="Extra credit", inr=_env_i("PRICE_EXTRA", 149), usd=_env_f("PRICE_EXTRA_USD", 2), credits=1)
|
| 89 |
+
FORMAT_FILE = dict(name="Journal formatting", inr=_env_i("PRICE_FORMAT", 99), usd=_env_f("PRICE_FORMAT_USD", 2))
|
| 90 |
+
EXTRA_DOC = SINGLE # older name
|
| 91 |
+
INR_NOTE = os.getenv("PRICE_INR_NOTE", "Prices include 18% GST.")
|
| 92 |
DEFAULT_CURRENCY = os.getenv("PRICE_CURRENCY", "INR").strip().upper()
|
| 93 |
+
_ITEMS = {"single": SINGLE, "pack": PACK, "extra": EXTRA, "format": FORMAT_FILE}
|
| 94 |
|
| 95 |
|
| 96 |
def _cur(currency):
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
def money(amount, cur="INR", per=""):
|
| 102 |
+
"""₹999 / $14 ('' -> Free)."""
|
| 103 |
if not amount:
|
| 104 |
return "Free"
|
| 105 |
return f"₹{amount:,.0f}{per}" if cur == "INR" else f"${amount:,.0f}{per}"
|
| 106 |
|
| 107 |
|
| 108 |
def price_of(item, cur="INR"):
|
| 109 |
+
p = _ITEMS.get(item) or PLANS[item]
|
| 110 |
return money(p["usd"] if cur == "USD" else p["inr"], cur)
|
| 111 |
|
| 112 |
|
| 113 |
def both(item):
|
| 114 |
+
"""'₹999 ($14)' with the default currency first."""
|
| 115 |
c1 = _cur(DEFAULT_CURRENCY)
|
| 116 |
c2 = "USD" if c1 == "INR" else "INR"
|
| 117 |
return f"{price_of(item, c1)} ({price_of(item, c2)})"
|
| 118 |
|
| 119 |
|
| 120 |
+
def credits_needed(words):
|
| 121 |
+
"""Credits a document of `words` words uses (one per started CREDIT_WORDS words)."""
|
| 122 |
+
return max(1, math.ceil((words or 0) / CREDIT_WORDS))
|
| 123 |
+
|
| 124 |
+
|
| 125 |
def per_day_text(p):
|
| 126 |
+
return f"{p['docs_month']} credits per month"
|
| 127 |
+
|
| 128 |
+
|
| 129 |
# ---------------------------------------------------------------- storage
|
| 130 |
def _db():
|
| 131 |
con = A._db()
|
|
|
|
| 133 |
for c, ddl in (("plan", "TEXT DEFAULT 'free'"), ("plan_until", "TEXT"), ("period_start", "TEXT"),
|
| 134 |
("period_docs", "INTEGER DEFAULT 0"), ("doc_credits", "INTEGER DEFAULT 0"),
|
| 135 |
("trial_used", "INTEGER DEFAULT 0"), ("plan_started", "TEXT"),
|
| 136 |
+
("plan_status", "TEXT DEFAULT 'free'"), ("credits_until", "TEXT")):
|
| 137 |
if c not in cols:
|
| 138 |
try:
|
| 139 |
con.execute(f"ALTER TABLE users ADD COLUMN {c} {ddl}")
|
|
|
|
| 147 |
con.execute("CREATE TABLE IF NOT EXISTS plan_history (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
| 148 |
"ts TEXT DEFAULT CURRENT_TIMESTAMP, email TEXT, plan TEXT, started TEXT, until TEXT, "
|
| 149 |
"event TEXT, note TEXT)")
|
| 150 |
+
con.execute("CREATE TABLE IF NOT EXISTS reruns (email TEXT, fkey TEXT, first_ts TEXT, runs INTEGER DEFAULT 0, "
|
| 151 |
+
"PRIMARY KEY (email, fkey))")
|
| 152 |
+
con.execute("CREATE TABLE IF NOT EXISTS daily_usage (email TEXT, day TEXT, tool TEXT, n INTEGER DEFAULT 0, "
|
| 153 |
+
"PRIMARY KEY (email, day, tool))")
|
| 154 |
return con
|
| 155 |
|
| 156 |
|
|
|
|
| 235 |
"""Everything the UI and the checks need about a user's plan."""
|
| 236 |
if A.is_admin(user):
|
| 237 |
return dict(key="admin", **PLANS["admin"], until=None, started=None, used=0, used_today=0, credits=0,
|
| 238 |
+
credits_until=None,
|
| 239 |
trial_left=0, paid=True, admin=True, status="admin", month_end=None)
|
| 240 |
if not user:
|
| 241 |
return dict(key="guest", **PLANS["guest"], until=None, started=None, used=0, used_today=0, credits=0,
|
| 242 |
+
credits_until=None,
|
| 243 |
trial_left=0, paid=False, admin=False, status="guest", month_end=None)
|
| 244 |
with _db() as con:
|
| 245 |
row = con.execute("SELECT plan, plan_until, plan_started, doc_credits, trial_used, plan_status, "
|
| 246 |
+
"period_start, period_docs, credits_until FROM users WHERE email=?",
|
| 247 |
+
(user["email"],)).fetchone()
|
| 248 |
+
plan, until, started, credits, trial, status, pstart, pdocs, cuntil = \
|
| 249 |
+
row or ("free", None, None, 0, 0, "free", None, 0, None)
|
| 250 |
+
if credits and cuntil and _parse(cuntil) <= _now(): # bought credits are valid CREDIT_MONTHS months
|
| 251 |
+
with _db() as con:
|
| 252 |
+
con.execute("UPDATE users SET doc_credits=0 WHERE email=?", (user["email"],))
|
| 253 |
+
con.execute("INSERT INTO plan_history (email, plan, started, until, event, note) VALUES (?,?,?,?,?,?)",
|
| 254 |
+
(user["email"], "credits", None, cuntil, "credits expired", f"{credits} credit(s)"))
|
| 255 |
+
credits = 0
|
| 256 |
if plan in PAID and until and _parse(until) <= _now():
|
| 257 |
expire_due(user["email"]) # month over, not renewed: switch it off now
|
| 258 |
plan, status = "free", "expired"
|
|
|
|
| 267 |
p = PLANS.get(plan, PLANS["free"])
|
| 268 |
return dict(key=plan if plan in PLANS else "free", **p, until=until if plan in PAID else None,
|
| 269 |
started=started if plan in PAID else None, used=used, used_today=used, credits=credits or 0,
|
| 270 |
+
credits_until=cuntil if credits else None,
|
| 271 |
trial_left=max(0, FREE_TRIAL_DOCS - (trial or 0)), month_end=month_end,
|
| 272 |
paid=plan in PAID or (credits or 0) > 0, admin=False,
|
| 273 |
status="active" if plan in PAID else (status or "free"))
|
| 274 |
|
| 275 |
|
| 276 |
def docs_available(st):
|
| 277 |
+
"""Credits the user can still spend: monthly allowance left + bought credits (+ free trial documents)."""
|
| 278 |
if st["key"] in PAID:
|
| 279 |
return max(0, st["docs_month"] - st["used"]) + st["credits"]
|
| 280 |
return st["credits"] + st["trial_left"]
|
|
|
|
| 285 |
return f"{reason} See the 💎 Pricing tab to upgrade."
|
| 286 |
|
| 287 |
|
| 288 |
+
def _ist_day():
|
| 289 |
+
return time.strftime("%Y-%m-%d", time.gmtime(_now() + 330 * 60))
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def _daily(email, tool):
|
| 293 |
+
with _db() as con:
|
| 294 |
+
r = con.execute("SELECT n FROM daily_usage WHERE email=? AND day=? AND tool=?",
|
| 295 |
+
(email, _ist_day(), tool)).fetchone()
|
| 296 |
+
return r[0] if r else 0
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _daily_add(email, tool):
|
| 300 |
+
with _db() as con:
|
| 301 |
+
k = con.execute("UPDATE daily_usage SET n=n+1 WHERE email=? AND day=? AND tool=?",
|
| 302 |
+
(email, _ist_day(), tool)).rowcount
|
| 303 |
+
if not k:
|
| 304 |
+
con.execute("INSERT INTO daily_usage (email, day, tool, n) VALUES (?,?,?,1)", (email, _ist_day(), tool))
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def _free_rerun(email, fkey):
|
| 308 |
+
"""True if this file was charged within RERUN_DAYS and still has free re-runs left."""
|
| 309 |
+
if not fkey:
|
| 310 |
+
return False
|
| 311 |
+
with _db() as con:
|
| 312 |
+
r = con.execute("SELECT first_ts, runs FROM reruns WHERE email=? AND fkey=?", (email, fkey)).fetchone()
|
| 313 |
+
return bool(r) and _parse(r[0]) + RERUN_DAYS * 86400 > _now() and (r[1] or 0) < FREE_RERUNS
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def _is_detector(tool):
|
| 317 |
+
return "detector" in str(tool or "").lower()
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def check_doc(user, words=None, tool="", fkey=None):
|
| 321 |
+
"""Before a document run: plan / credits / trial / word cap. Raises A.Upsell with a friendly message."""
|
| 322 |
st = state(user)
|
| 323 |
if st["admin"]:
|
| 324 |
return
|
| 325 |
+
e = (user or {}).get("email")
|
| 326 |
+
if _is_detector(tool): # detector file reports: unlimited on plans, a few a day free
|
| 327 |
+
if st["key"] in PAID or st["credits"] > 0:
|
| 328 |
+
return
|
| 329 |
+
if _daily(e, "detector") >= FREE_DETECT_DAY:
|
| 330 |
+
raise A.Upsell(f"The free account includes {FREE_DETECT_DAY} detector file reports a day. Paste text "
|
| 331 |
+
f"for more checks today, or take the Scholar plan ({both('scholar')}/month) for "
|
| 332 |
+
"unlimited reports.", kind="limit", title="Today's free reports are used")
|
| 333 |
+
return
|
| 334 |
+
if e and _free_rerun(e, fkey):
|
| 335 |
+
return # free re-run of a file charged in the last days
|
| 336 |
+
need = credits_needed(words)
|
| 337 |
+
cap = st["doc_words"] if st["key"] in PAID or st["credits"] > 0 else PLANS["free"]["doc_words"]
|
| 338 |
+
if st["key"] not in PAID and st["credits"] <= 0 and st["trial_left"] > 0:
|
| 339 |
+
if words and words > cap:
|
| 340 |
+
raise A.Upsell(f"This document has {words:,} words; the free trial covers documents up to {cap:,} words. "
|
| 341 |
+
f"A single credit ({both('single')}) covers up to {CREDIT_WORDS:,} words.",
|
| 342 |
+
title="This document is longer than the free trial")
|
| 343 |
+
return
|
| 344 |
+
have = docs_available(st)
|
| 345 |
+
if have < need:
|
| 346 |
+
if st["key"] in PAID:
|
| 347 |
+
raise A.Upsell(f"This document needs {need} credit{'s' if need > 1 else ''} ({words or 0:,} words; one "
|
| 348 |
+
f"credit = {CREDIT_WORDS:,} words) and you have {have} left this month. Add extra credits "
|
| 349 |
+
f"at {both('extra')} each; your monthly credits start again on "
|
| 350 |
+
f"{_ist_date(st['month_end'])}.", kind="extra", title="Monthly credits used up")
|
| 351 |
+
msg = ("Your plan was not renewed, so it has been switched off. Renew it on the 💎 Pricing tab, or buy "
|
| 352 |
+
f"credits ({both('single')} each, {both('pack')} for 5)." if st["status"] == "expired" else
|
| 353 |
+
"Your subscription has been ended. Please contact us, or choose an option on the 💎 Pricing tab."
|
| 354 |
+
if st["status"] == "terminated" else
|
| 355 |
+
f"This document needs {need} credit{'s' if need > 1 else ''} (one credit = {CREDIT_WORDS:,} words). "
|
| 356 |
+
f"Buy a single credit for {both('single')}, a pack of 5 for {both('pack')}, or take Scholar: "
|
| 357 |
+
f"{PLANS['scholar']['docs_month']} credits for {both('scholar')}/month.")
|
| 358 |
+
raise A.Upsell(msg, title="Your plan has ended" if st["status"] in ("expired", "terminated") else
|
| 359 |
+
"Credits needed")
|
| 360 |
+
biggest = st["doc_words"] if st["key"] in PAID else PLANS["scholar"]["doc_words"] # bought credits: any size
|
| 361 |
+
if words and words > biggest:
|
| 362 |
+
raise A.Upsell(f"This document has {words:,} words; the largest document is {biggest:,} words. "
|
| 363 |
+
"Split it into chapters.", title="This document is very long")
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def doc_cost_text(user, words, tool=""):
|
| 367 |
+
"""'This run uses 2 credits (21,400 words)' for the upload area ('' when not applicable)."""
|
| 368 |
+
if not words or _is_detector(tool):
|
| 369 |
+
return ""
|
| 370 |
+
st = state(user)
|
| 371 |
+
if st["admin"]:
|
| 372 |
+
return ""
|
| 373 |
+
n = credits_needed(words)
|
| 374 |
+
return f"This document ({words:,} words) uses {n} credit{'s' if n > 1 else ''}; you have {docs_available(st)}."
|
| 375 |
|
| 376 |
|
| 377 |
def paste_limit(user):
|
|
|
|
| 382 |
st = state(user)
|
| 383 |
n = len((text or "").split())
|
| 384 |
if n > st["paste_words"]:
|
| 385 |
+
if st["key"] not in PAID:
|
| 386 |
+
raise A.Upsell(f"Free pastes are up to {st['paste_words']} words (yours: {n:,}). Scholar allows "
|
| 387 |
+
f"{PLANS['scholar']['paste_words']:,} words and Pro {PLANS['pro']['paste_words']:,} per "
|
| 388 |
+
"paste, or upload a .docx with a credit.", title="Your text is longer than the free limit")
|
| 389 |
raise A.Upsell(f"Your plan allows {st['paste_words']:,} words per paste (yours: {n:,}). Upload a .docx for "
|
| 390 |
"longer text.", kind="limit", title="Your text is a bit long")
|
| 391 |
|
|
|
|
| 394 |
return state(user)["pastes"]
|
| 395 |
|
| 396 |
|
| 397 |
+
def max_passes(user, requested, document=False):
|
| 398 |
+
"""Smart Humanize passes: the plan's limit; the free trial document gets the full automatic passes."""
|
| 399 |
+
st = state(user)
|
| 400 |
+
cap = st["passes"]
|
| 401 |
+
if document and (st["credits"] > 0 or (st["key"] == "free" and st["trial_left"] > 0)):
|
| 402 |
+
cap = 10
|
| 403 |
+
return int(min(int(requested), cap))
|
| 404 |
|
| 405 |
|
| 406 |
def doc_words(path):
|
| 407 |
+
"""Quick word count of an uploaded file (for credits and the word cap)."""
|
| 408 |
try:
|
| 409 |
p = str(path).lower()
|
| 410 |
if p.endswith(".docx"):
|
|
|
|
| 427 |
|
| 428 |
|
| 429 |
# ---------------------------------------------------------------- consume (after a successful run)
|
| 430 |
+
def consume_doc(user, words=None, tool="", fkey=None):
|
| 431 |
+
"""Charge a successful document run: free re-run if allowed; detector files only count on the free account;
|
| 432 |
+
otherwise credits_needed(words) from the month's allowance, then bought credits, then the free trial."""
|
| 433 |
st = state(user)
|
| 434 |
if st["admin"] or not user:
|
| 435 |
return
|
| 436 |
e = user["email"]
|
| 437 |
+
if _is_detector(tool):
|
| 438 |
+
if st["key"] not in PAID and st["credits"] <= 0:
|
| 439 |
+
_daily_add(e, "detector")
|
| 440 |
+
return
|
| 441 |
with _db() as con:
|
| 442 |
+
if fkey:
|
| 443 |
+
r = con.execute("SELECT first_ts, runs FROM reruns WHERE email=? AND fkey=?", (e, fkey)).fetchone()
|
| 444 |
+
if r and _parse(r[0]) + RERUN_DAYS * 86400 > _now() and (r[1] or 0) < FREE_RERUNS:
|
| 445 |
+
con.execute("UPDATE reruns SET runs=COALESCE(runs,0)+1 WHERE email=? AND fkey=?", (e, fkey))
|
| 446 |
+
A.request_sync()
|
| 447 |
+
return
|
| 448 |
+
need = credits_needed(words)
|
| 449 |
+
if st["key"] not in PAID and st["credits"] <= 0 and st["trial_left"] > 0:
|
| 450 |
con.execute("UPDATE users SET trial_used=COALESCE(trial_used,0)+1 WHERE email=?", (e,))
|
| 451 |
+
else:
|
| 452 |
+
from_month = min(need, max(0, st["docs_month"] - st["used"])) if st["key"] in PAID else 0
|
| 453 |
+
if from_month:
|
| 454 |
+
con.execute("UPDATE users SET period_docs=COALESCE(period_docs,0)+? WHERE email=?", (from_month, e))
|
| 455 |
+
rest = need - from_month
|
| 456 |
+
if rest:
|
| 457 |
+
con.execute("UPDATE users SET doc_credits=MAX(0, COALESCE(doc_credits,0)-?) WHERE email=?", (rest, e))
|
| 458 |
+
if fkey:
|
| 459 |
+
con.execute("INSERT OR REPLACE INTO reruns (email, fkey, first_ts, runs) VALUES (?,?,?,0)",
|
| 460 |
+
(e, fkey, _ts(_now())))
|
| 461 |
A.request_sync()
|
| 462 |
|
| 463 |
|
|
|
|
| 538 |
|
| 539 |
|
| 540 |
def add_credits(email, n, source="admin", amount="", note=""):
|
| 541 |
+
"""Add bought credits (single, pack or extra); all of the user's bought credits are then valid CREDIT_MONTHS
|
| 542 |
+
months from today."""
|
| 543 |
email = (email or "").strip().lower()
|
| 544 |
+
t = _now()
|
| 545 |
+
for _ in range(CREDIT_MONTHS):
|
| 546 |
+
t = add_month(t)
|
| 547 |
with _db() as con:
|
| 548 |
+
k = con.execute("UPDATE users SET doc_credits=COALESCE(doc_credits,0)+?, credits_until=? WHERE email=?",
|
| 549 |
+
(int(n), _ts(t), email)).rowcount
|
| 550 |
if not k:
|
| 551 |
return False, f"No user found with {email}."
|
| 552 |
con.execute("INSERT INTO payments (email, item, amount, source, note) VALUES (?,?,?,?,?)",
|
| 553 |
+
(email, f"{int(n)} credit(s)", amount, source, note))
|
| 554 |
con.execute("UPDATE upgrade_requests SET status='done' WHERE email=? AND status='open'", (email,))
|
| 555 |
A.request_sync()
|
| 556 |
+
return True, f"Added {int(n)} credit(s) to {email}, valid until {_ist_date(_ts(t))}."
|
| 557 |
|
| 558 |
|
| 559 |
def request_upgrade(user, item):
|
|
|
|
| 582 |
if st["admin"]:
|
| 583 |
return "Admin — everything is free and unlimited."
|
| 584 |
if not user:
|
| 585 |
+
return (f"Guest: {st['paste_words']}-word pastes, {st['pastes']} runs a day. Sign up free for a trial "
|
| 586 |
+
f"document (up to {PLANS['free']['doc_words']:,} words) and {FREE_DETECT_DAY} detector reports a day.")
|
| 587 |
parts = []
|
| 588 |
if st["key"] in PAID:
|
| 589 |
+
parts.append(f"{st['name']} plan: {min(st['used'], st['docs_month'])}/{st['docs_month']} credits used this "
|
| 590 |
+
f"month (new month starts {_ist_date(st['month_end'])}) · paid until {_ist_date(st['until'])}, "
|
| 591 |
+
"renew by then to keep it")
|
| 592 |
elif st["status"] == "expired":
|
| 593 |
parts.append("Your plan ended (not renewed). Renew on the Pricing tab")
|
| 594 |
elif st["status"] == "terminated":
|
| 595 |
parts.append("Your subscription has been ended. Contact us or choose an option on the Pricing tab")
|
| 596 |
else:
|
| 597 |
+
parts.append(f"Free plan: {st['trial_left']} of {FREE_TRIAL_DOCS} trial document(s) left")
|
| 598 |
if st["credits"]:
|
| 599 |
+
parts.append(f"{st['credits']} credit(s) available until {_ist_date(st.get('credits_until'))}")
|
| 600 |
+
parts.append(f"1 credit = 1 document up to {CREDIT_WORDS:,} words")
|
| 601 |
return " · ".join(parts)
|
| 602 |
|
| 603 |
|
| 604 |
def nudge(user, document):
|
| 605 |
"""Shown under a finished run for guests and free users."""
|
| 606 |
st = state(user)
|
| 607 |
+
if st["admin"] or st["key"] in PAID or st["credits"] > 0:
|
| 608 |
return ""
|
| 609 |
+
offer = (f"1 credit {both('single')} · 5 credits {both('pack')} · Scholar {both('scholar')}/month "
|
| 610 |
+
f"({PLANS['scholar']['docs_month']} credits).")
|
|
|
|
| 611 |
if not user:
|
| 612 |
+
msg = (f"Want to check a whole chapter? <b>Sign up free</b> for a trial document (up to "
|
| 613 |
+
f"{PLANS['free']['doc_words']:,} words): layout kept, highlighted PDF report included.")
|
| 614 |
elif st["status"] in ("expired", "terminated"):
|
| 615 |
msg = f"<b>Your plan has ended.</b> {offer}"
|
|
|
|
|
|
|
| 616 |
elif st["trial_left"] > 0:
|
| 617 |
+
msg = f"<b>Your free trial document is waiting.</b> {offer}"
|
| 618 |
else:
|
| 619 |
+
msg = f"<b>Your free trial is used.</b> {offer}"
|
| 620 |
return f'<div class="cs-nudge">💎 {msg} Open the <b>Pricing</b> tab.</div>'
|
| 621 |
|
| 622 |
|
|
|
|
| 624 |
cur = _cur(currency)
|
| 625 |
other = "USD" if cur == "INR" else "INR"
|
| 626 |
where = "outside India" if other == "USD" else "in India"
|
| 627 |
+
sc, pr = PLANS["scholar"], PLANS["pro"]
|
| 628 |
+
common = ["Smart Humanize + AI detector PDF report", "Rigorous proofread", "Word layout, tables, citations and "
|
| 629 |
+
"equations kept", f"{FREE_RERUNS} free re-runs of the same file within {RERUN_DAYS} days"]
|
| 630 |
|
| 631 |
+
def card(title, price, per, alt, feats, featured=False, tag_text="Best value"):
|
| 632 |
li = "".join(f"<li>{html.escape(f)}</li>" for f in feats)
|
| 633 |
+
tag = f'<div class="cs-pop">{html.escape(tag_text)}</div>' if featured else ""
|
| 634 |
return (f'<div class="cs-price{" cs-featured" if featured else ""}">{tag}<div class="cs-pname">'
|
| 635 |
f'{html.escape(title)}</div><div class="cs-pval">{price}<small>{per}</small></div>'
|
| 636 |
f'<div class="cs-inr">{alt} {where}</div><ul>{li}</ul></div>')
|
| 637 |
|
| 638 |
+
cards = [
|
| 639 |
+
card("Single credit", price_of("single", cur), "/credit", f"{price_of('single', other)}/credit",
|
| 640 |
+
[f"1 document up to {CREDIT_WORDS:,} words", f"Valid {CREDIT_MONTHS} months",
|
| 641 |
+
"Journal formatting of the same file included"] + common),
|
| 642 |
+
card("Pack of 5", price_of("pack", cur), "/5 credits", f"{price_of('pack', other)}/5 credits",
|
| 643 |
+
["5 documents (a full thesis)", f"Valid {CREDIT_MONTHS} months", "Journal formatting of each file "
|
| 644 |
+
"included"] + common),
|
| 645 |
+
card("Scholar", price_of("scholar", cur), "/month", f"{price_of('scholar', other)}/month",
|
| 646 |
+
[f"{sc['docs_month']} credits every month", f"Pasted text up to {sc['paste_words']:,} words, unlimited",
|
| 647 |
+
"Unlimited AI detector reports", f"{sc['format_month']} journal-formatting files a month",
|
| 648 |
+
f"Extra credits {price_of('extra', cur)} each"] + common, featured=True),
|
| 649 |
+
card("Pro", price_of("pro", cur), "/month", f"{price_of('pro', other)}/month",
|
| 650 |
+
[f"{pr['docs_month']} credits every month", f"Pasted text up to {pr['paste_words']:,} words, unlimited",
|
| 651 |
+
"Unlimited AI detector reports", f"{pr['format_month']} journal-formatting files a month",
|
| 652 |
+
"Priority queue", f"Extra credits {price_of('extra', cur)} each"] + common,
|
| 653 |
+
featured=True, tag_text="For heavy users"),
|
| 654 |
+
]
|
| 655 |
promo = f'<div class="cs-promo">🎉 {html.escape(PROMO_TEXT)}</div>' if PROMO_TEXT else ""
|
| 656 |
+
extra = (f'<div class="cs-gauge-cap" style="text-align:left;max-width:1100px;margin:10px auto 0">'
|
| 657 |
+
f'<b>Free account:</b> pasted text up to {PLANS["free"]["paste_words"]} words ({PLANS["free"]["pastes"]} a '
|
| 658 |
+
f'day), {FREE_TRIAL_DOCS} trial document up to {PLANS["free"]["doc_words"]:,} words, {FREE_DETECT_DAY} '
|
| 659 |
+
f'detector reports a day, 1 journal-formatting file a month. '
|
| 660 |
+
f'<b>Journal Finder:</b> free for everyone. <b>Journal formatting</b> on its own: '
|
| 661 |
+
f'{price_of("format", cur)} per file. <b>Institutions and agencies:</b> from '
|
| 662 |
+
f'{price_of("institution", cur)}/month for {PLANS["institution"]["docs_month"]} credits, several logins and '
|
| 663 |
+
f'a GST invoice, contact us.'
|
| 664 |
+
+ (f' {html.escape(INR_NOTE)}' if INR_NOTE and cur == "INR" else "") + "</div>")
|
| 665 |
+
return (promo + '<div class="cs-prices" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));'
|
| 666 |
+
'max-width:1100px;margin:0 auto">' + "".join(cards) + "</div>" + extra)
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
BUY_ITEMS = ("Single credit", "Pack of 5 credits", "Scholar plan", "Pro plan")
|
| 670 |
|
| 671 |
|
| 672 |
def price_buttons(currency=None):
|
| 673 |
+
"""Labels of the four buy buttons in the chosen currency (same order as BUY_ITEMS)."""
|
| 674 |
cur = _cur(currency)
|
| 675 |
+
return (f"Buy 1 credit — {price_of('single', cur)}", f"Buy 5 credits — {price_of('pack', cur)}",
|
| 676 |
+
f"Scholar — {price_of('scholar', cur)}/month", f"Pro — {price_of('pro', cur)}/month")
|
| 677 |
|
| 678 |
|
| 679 |
def pay_instructions(user, item):
|
| 680 |
+
"""Until online payment is live: how to pay + the request is logged for the admin."""
|
| 681 |
if A.is_admin(user):
|
| 682 |
return '<div class="cs-authmsg cs-ok">You are an admin — every feature is free for you.</div>'
|
| 683 |
if not user:
|
| 684 |
return ('<div class="cs-authmsg cs-bad">Please sign in (or create a free account) first, so we can '
|
| 685 |
+
'activate it on your account.</div>')
|
| 686 |
request_upgrade(user, item)
|
| 687 |
links = []
|
| 688 |
if PAY_WHATSAPP:
|
| 689 |
+
txt = f"Hi, I want to buy: {item} for {user['email']}".replace(" ", "%20")
|
|
|
|
| 690 |
links.append(f'<a class="cs-dl" target="_blank" href="https://wa.me/{PAY_WHATSAPP}?text={txt}">'
|
| 691 |
f'💬 Pay via WhatsApp / UPI</a>')
|
| 692 |
if PAY_EMAIL:
|
| 693 |
+
links.append(f'<a class="cs-dl" href="mailto:{PAY_EMAIL}?subject={item.replace(" ", "%20")}'
|
| 694 |
f'&body=Account:%20{user["email"]}">✉️ Email us to pay</a>')
|
| 695 |
contact = "".join(links) or (f'<div class="cs-gauge-cap" style="text-align:left">Contact us through '
|
| 696 |
f'<a href="https://cssoftwaresolutions.in" target="_blank">cssoftwaresolutions.in</a>.</div>')
|
| 697 |
return (f'<div class="cs-authmsg cs-ok"><b>{html.escape(item)}</b> requested for '
|
| 698 |
f'<b>{html.escape(user["email"])}</b>. Online card payment is launching shortly; for now pay by UPI or '
|
| 699 |
+
f'bank transfer and it is switched on the same day.</div>{contact}')
|
| 700 |
|
| 701 |
|
| 702 |
# ---------------------------------------------------------------- admin
|