Spaces:
Running
Running
Download server.py from Elias207/imageeditingpro1: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/Elias207/imageeditingpro1/resolve/main/server.py
- Command line
-
hf download hf://spaces/Elias207/imageeditingpro1/server.py
-
curl -L -o server.py https://huggingface.co/spaces/Elias207/imageeditingpro1/resolve/main/server.py
11 kB
| import asyncio | |
| import hmac | |
| import os | |
| import re | |
| import threading | |
| import time | |
| from pathlib import Path | |
| from typing import List, Optional | |
| from fastapi import FastAPI, Header, HTTPException | |
| from fastapi.responses import FileResponse, HTMLResponse, JSONResponse | |
| from pydantic import BaseModel | |
| import builder | |
| import store | |
| app = FastAPI(title="Lesson content server") | |
| state = {"status": "idle", "log": [], "errors": {}, "finished": None, "voices": {}, "fallback": {}} | |
| lock = threading.Lock() | |
| def _log(msg): | |
| state["log"].append(msg) | |
| del state["log"][:-200] | |
| def run_build(): | |
| if not lock.acquire(blocking=False): | |
| return | |
| try: | |
| state.update(status="building", log=[], errors={}) | |
| # صداهای قبلی را از ذخیرهی پایدار برمیگردانیم. اگر نشد، ساخت را متوقف میکنیم تا چیزی از نو ساخته و بازنویسی نشود. | |
| if not store.pull(_log): | |
| _log("ساخت متوقف شد تا صداهای قبلی بازنویسی نشوند. کمی بعد «ساخت دوباره» را بزنید.") | |
| state["status"] = "failed" | |
| return | |
| def sync(): | |
| if builder.STATE["dirty"] and store.push(_log): | |
| builder.STATE["dirty"] = False | |
| _, errors = asyncio.run(builder.build_all(_log, sync)) | |
| sync() | |
| fb = {k: sum(1 for i in v["items"] if i["fb"]) for k, v in builder.LESSON_AUDIO.items()} | |
| state.update(status="ready", errors=errors, voices=dict(builder.INFO), fallback={k: n for k, n in fb.items() if n}, | |
| finished=time.strftime("%Y-%m-%d %H:%M:%S")) | |
| if state["fallback"]: | |
| _log("⚠️ جملههایی با صدای جایگزین ساخته شدهاند: " + ", ".join(f"{k}({n})" for k, n in state["fallback"].items()) | |
| + " . در صفحهی «بررسی صداها» میتوانید دوباره با Kokoro بسازید.") | |
| except Exception as e: # noqa: BLE001 | |
| _log(f"خطای کلی: {e}") | |
| state["status"] = "failed" | |
| finally: | |
| lock.release() | |
| def startup(): | |
| threading.Thread(target=run_build, daemon=True).start() | |
| def check_password(p): | |
| expected = os.getenv("REBUILD_PASSWORD", "") | |
| if expected and not hmac.compare_digest(p or "", expected): | |
| raise HTTPException(401, "رمز اشتباه است") | |
| def manifest(): | |
| f = builder.OUT / "manifest.json" | |
| if not f.exists(): | |
| return JSONResponse({"status": state["status"]}, status_code=503) | |
| return FileResponse(f, media_type="application/json", headers={"Cache-Control": "no-cache"}) | |
| STRINGS_FILE = Path(__file__).parent / "strings.json" | |
| def strings(): | |
| """متنهای برنامهی اندروید: { "متن فعلی": "متن جدید" }. اپ هنگام باز شدن آن را میگیرد.""" | |
| if not STRINGS_FILE.exists(): | |
| return JSONResponse({}, headers={"Cache-Control": "no-cache"}) | |
| return FileResponse(STRINGS_FILE, media_type="application/json", headers={"Cache-Control": "no-cache"}) | |
| def pack(name: str): | |
| if not re.fullmatch(r"[A-Za-z0-9_.-]+\.zip", name): | |
| raise HTTPException(400, "bad name") | |
| f = builder.OUT / "packs" / name | |
| if not f.exists(): | |
| raise HTTPException(404, "not found") | |
| return FileResponse(f, media_type="application/zip") | |
| def status(): | |
| return state | |
| def rebuild(x_password: str = Header(default="")): | |
| check_password(x_password) | |
| if state["status"] == "building": | |
| return {"ok": False, "msg": "ساخت در حال انجام است"} | |
| threading.Thread(target=run_build, daemon=True).start() | |
| return {"ok": True} | |
| class RegenBody(BaseModel): | |
| lesson: str | |
| texts: Optional[List[str]] = None # خالی = همهی متنهای درس | |
| def api_regen(body: RegenBody, x_password: str = Header(default="")): | |
| """صدای کل یک درس یا فقط چند جمله/واژه را دوباره میسازد. بقیهی درسها و صداها دست نمیخورند.""" | |
| check_password(x_password) | |
| if state["status"] == "building": | |
| raise HTTPException(409, "ساخت در حال انجام است؛ بعد از پایانش دوباره تلاش کنید") | |
| try: | |
| n = builder.regen(body.lesson, body.texts) | |
| except Exception as e: # noqa: BLE001 | |
| raise HTTPException(400, str(e)) | |
| threading.Thread(target=run_build, daemon=True).start() | |
| return {"ok": True, "count": n} | |
| def api_lessons(): | |
| return builder.LESSON_AUDIO | |
| def audio(name: str): | |
| if not re.fullmatch(r"[A-Za-z0-9_-]+\.(mp3|wav|ogg|m4a|flac)", name): | |
| raise HTTPException(400, "bad name") | |
| f = builder.CACHE / name | |
| if not f.exists(): | |
| raise HTTPException(404, "not found") | |
| return FileResponse(f) | |
| REVIEW = r"""<!doctype html><html lang="fa" dir="rtl"><meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"><title>بررسی صداها</title> | |
| <style>body{font-family:system-ui,Tahoma;max-width:820px;margin:20px auto;padding:0 12px;background:#f4f9f9;color:#10282b} | |
| details{background:#fff;border-radius:12px;padding:10px 14px;margin:10px 0;box-shadow:0 1px 4px #0001} | |
| summary{cursor:pointer;font-weight:bold} | |
| .row{display:flex;gap:8px;align-items:center;padding:7px 0;border-top:1px solid #e3eeee;flex-wrap:wrap} | |
| .row .t{flex:1 1 260px;direction:ltr;text-align:left;font-size:14px} | |
| .k{font-size:11px;background:#dff1f3;border-radius:6px;padding:1px 6px}.fb{background:#ffe3a3} | |
| button{background:#0b7f8b;color:#fff;border:0;border-radius:8px;padding:6px 12px;cursor:pointer} | |
| button.alt{background:#fff;color:#0b7f8b;border:1px solid #0b7f8b} | |
| input{padding:8px;border-radius:8px;border:1px solid #9bb}</style> | |
| <h3>بررسی صداها</h3> | |
| <p><a href="./">← صفحهی اصلی</a> · <input id="pw" type="password" placeholder="رمز (اگر تعریف کردهاید)"></p> | |
| <p style="font-size:13px">هر متن را پخش کنید. اگر تلفظ بد بود «بازسازی» را بزنید. فقط همان صدا ساخته میشود و بقیهی درسها دست نمیخورند. | |
| اگر بعد از بازسازی هم همانطور خوانده شد، در فایل درس کنار آن متن فیلد <code>"tts"</code> بگذارید (مثلاً املای دیگر یا علامتگذاری دیگر). | |
| بعد از تغییر صدا، <code>version</code> همان درس را بالا ببرید تا کاربران قدیمی هم نسخهی تازه را بگیرند.</p> | |
| <div id="box">در حال خواندن…</div> | |
| <script> | |
| async function regen(lesson,texts){ | |
| const r=await fetch('api/regen',{method:'POST',headers:{'Content-Type':'application/json','X-Password':pw.value}, | |
| body:JSON.stringify({lesson:lesson,texts:texts})}); | |
| if(!r.ok){let m='خطا';try{m=(await r.json()).detail||m}catch(e){}alert(m);return} | |
| alert('ساخت شروع شد. چند دقیقه بعد این صفحه را تازه کنید.');} | |
| (async()=>{ | |
| const d=await (await fetch('api/lessons')).json(); box.textContent=''; | |
| const ids=Object.keys(d); if(!ids.length){box.textContent='هنوز ساخته نشده؛ چند دقیقه بعد دوباره باز کنید.';return} | |
| ids.forEach(id=>{ | |
| const L=d[id], det=document.createElement('details'), sm=document.createElement('summary'); | |
| const nfb=L.items.filter(i=>i.fb).length; | |
| sm.textContent=id+' — '+L.title+' ('+L.items.length+' صدا'+(nfb?('، '+nfb+' با صدای جایگزین'):'')+')'; | |
| det.appendChild(sm); | |
| const all=document.createElement('button');all.className='alt';all.textContent='بازسازی همهی صداهای این درس'; | |
| all.onclick=()=>{if(confirm('همهی صداهای درس '+id+' دوباره ساخته شود؟'))regen(id,null)};det.appendChild(all); | |
| L.items.forEach(it=>{ | |
| const row=document.createElement('div');row.className='row'; | |
| const t=document.createElement('span');t.className='t';t.textContent=it.t; | |
| const k=document.createElement('span');k.className='k'+(it.fb?' fb':'');k.textContent=it.fb?'جایگزین':(it.k==='w'?'واژه':'جمله'); | |
| const a=document.createElement('audio');a.controls=true;a.preload='none';a.src='audio/'+it.f; | |
| const b=document.createElement('button');b.textContent='بازسازی';b.onclick=()=>regen(id,[it.t]); | |
| row.append(t,k,a,b);det.appendChild(row);}); | |
| box.appendChild(det);}); | |
| })(); | |
| </script></html>""" | |
| def review(): | |
| return REVIEW | |
| PAGE = """<!doctype html><html lang="fa" dir="rtl"><meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"><title>سرور درسها</title> | |
| <style>body{font-family:system-ui,Tahoma;max-width:720px;margin:24px auto;padding:0 16px;background:#f4f9f9;color:#10282b} | |
| .card{background:#fff;border-radius:14px;padding:16px;margin:12px 0;box-shadow:0 1px 4px #0001} | |
| button{background:#0b7f8b;color:#fff;border:0;border-radius:10px;padding:10px 18px;font-size:15px;cursor:pointer} | |
| input{padding:9px;border-radius:8px;border:1px solid #9bb;font-size:15px} | |
| pre{background:#10282b;color:#cfe;padding:12px;border-radius:10px;max-height:300px;overflow:auto;direction:ltr;text-align:left;white-space:pre-wrap} | |
| .ok{color:#1e9e6a}.bad{color:#d9482b}</style> | |
| <h2>سرور درسها</h2> | |
| <div class="card"><div>وضعیت: <b id="st">…</b></div><div id="fin"></div> | |
| <p><a href="review">🎧 بررسی صداها و بازسازی</a></p> | |
| <p>آدرس فهرست: <code id="url" dir="ltr"></code></p> | |
| <p><input id="pw" type="password" placeholder="رمز (اگر تعریف کردهاید)"> <button onclick="rb()">ساخت دوبارهی درسها و صداها</button></p></div> | |
| <div class="card"><b>گزارش</b><pre id="log"></pre><div id="err" class="bad"></div></div> | |
| <script> | |
| document.getElementById('url').textContent=location.origin+'/manifest.json'; | |
| async function load(){const s=await (await fetch('status')).json(); | |
| const m={idle:'آماده',building:'در حال ساخت…',ready:'آماده ✅',failed:'ناموفق ❌'}; | |
| st.textContent=m[s.status]||s.status;fin.textContent=(s.finished?('آخرین ساخت: '+s.finished):'')+(s.voices&&s.voices.words?(' | واژهها: '+s.voices.words+' | جملهها: '+s.voices.sentences):''); | |
| log.textContent=s.log.join('\\n');err.textContent=Object.entries(s.errors).map(([k,v])=>k+': '+v).join('\\n');} | |
| async function rb(){const r=await fetch('rebuild',{method:'POST',headers:{'X-Password':pw.value}}); | |
| if(!r.ok){alert('رمز اشتباه است');return}load()} | |
| load();setInterval(load,3000); | |
| </script></html>""" | |
| def home(): | |
| return PAGE | |