diff --git a/app.py b/app.py index 3d75445..6176616 100644 --- a/app.py +++ b/app.py @@ -22,7 +22,10 @@ UPLOADS_DIR.mkdir(exist_ok=True) THUMBNAILS_DIR = BASE / "thumbnails" THUMBNAILS_DIR.mkdir(exist_ok=True) +SHARED_DIR = BASE / "shared" +SHARED_DIR.mkdir(exist_ok=True) META_FILE = UPLOADS_DIR / "metadata.json" +_SHARE_TTL = 7 * 24 * 3600 # 7 days DH_API = "http://154.17.17.154:18801" OPENAI_TTS_URL = "https://api.openai.com/v1/audio/speech" @@ -682,6 +685,40 @@ async def ai_talk( ) +def _cleanup_shared(): + """Lazy cleanup: remove shared files older than 7 days.""" + now = time.time() + for f in SHARED_DIR.iterdir(): + if f.is_file() and (now - f.stat().st_mtime) > _SHARE_TTL: + f.unlink(missing_ok=True) + + +@app.post("/api/share") +async def share_video(video: UploadFile = File(...)): + _cleanup_shared() + share_id = uuid.uuid4().hex[:10] + path = SHARED_DIR / f"{share_id}.mp4" + data = await video.read() + if len(data) < 500: + raise HTTPException(400, "视频文件无效") + if len(data) > 50 * 1024 * 1024: + raise HTTPException(400, "视频文件过大(最大50MB)") + path.write_bytes(data) + return {"share_id": share_id} + + +@app.get("/api/shared/{share_id}") +async def get_shared_video(share_id: str): + _cleanup_shared() + # Sanitize share_id to prevent path traversal + safe_id = "".join(c for c in share_id if c.isalnum()) + path = SHARED_DIR / f"{safe_id}.mp4" + if not path.exists(): + raise HTTPException(404, "分享链接已过期或不存在") + return FileResponse(path, media_type="video/mp4", + headers={"Cache-Control": "public, max-age=3600"}) + + @app.get("/api/services") async def services_proxy(): try: diff --git a/static/app.js b/static/app.js index a848f3a..ec306da 100644 --- a/static/app.js +++ b/static/app.js @@ -26,6 +26,7 @@ let currentMode = "direct"; let currentEngine = "sadtalker"; let currentCategory = null; let currentVideoUrl = null; +let currentVideoBlob = null; let videoHistory = []; const MAX_HISTORY = 5; let voiceLabels = {}; @@ -271,6 +272,7 @@ async function generate() { llmBox.classList.add("hidden"); timeBadge.classList.add("hidden"); $("#download-btn").classList.add("hidden"); + $("#share-btn").classList.add("hidden"); // Reset steps setStep(0); @@ -368,6 +370,8 @@ async function generate() { timeBadge.textContent = elapsed + "s"; timeBadge.classList.remove("hidden"); $("#download-btn").classList.remove("hidden"); + $("#share-btn").classList.remove("hidden"); + currentVideoBlob = blob; addToHistory(url, avatarData[selectedAvatar]?.name || selectedAvatar, spokenText); @@ -653,6 +657,32 @@ $("#download-btn").addEventListener("click", () => { document.body.removeChild(a); }); +/* ── Share button ── */ +$("#share-btn").addEventListener("click", async () => { + if (!currentVideoBlob) return; + const btn = $("#share-btn"); + btn.disabled = true; + try { + const form = new FormData(); + form.append("video", currentVideoBlob, "video.mp4"); + const resp = await fetchWithRetry("/api/share", { method: "POST", body: form }); + if (!resp.ok) throw new Error("分享失败"); + const data = await resp.json(); + const shareUrl = `${location.origin}/api/shared/${data.share_id}`; + try { + await navigator.clipboard.writeText(shareUrl); + toast("链接已复制"); + } catch { + // Fallback: prompt + prompt("复制此链接分享:", shareUrl); + } + } catch (e) { + toast(e.message || "分享失败", 3000); + } finally { + btn.disabled = false; + } +}); + /* ── Hamburger menu (mobile) ── */ (() => { const hamburger = $("#hamburger"); diff --git a/static/index.html b/static/index.html index 8c2dd3c..28ce17b 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,7 @@