gtfgffg commited on
Commit
8a93ca3
·
verified ·
1 Parent(s): 3c8d8af

Upload 7 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # system deps (ffmpeg for moviepy)
4
+ RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt ./
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # copy source
12
+ COPY app ./app
13
+
14
+ # make a jobs dir for uploads
15
+ RUN mkdir -p /app/jobs
16
+
17
+ ENV PORT=8080
18
+ EXPOSE 8080
19
+
20
+ CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8080"]
app/__init__.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, UploadFile, File, Form
2
+ from fastapi.responses import HTMLResponse, JSONResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+ from starlette.datastructures import UploadFile as StarletteUploadFile
6
+ import os, uuid, shutil
7
+ from typing import Optional
8
+
9
+ app = FastAPI(title="FlexCut — تدوین هوشمند ویدیو (وب)")
10
+
11
+ # mount static and templates
12
+ app.mount("/static", StaticFiles(directory="app/static"), name="static")
13
+ templates = Jinja2Templates(directory="app/templates")
14
+
15
+ JOBS_DIR = "/app/jobs" if os.path.exists("/app/jobs") else os.path.join(os.getcwd(), "jobs")
16
+ os.makedirs(JOBS_DIR, exist_ok=True)
17
+
18
+ @app.get("/", response_class=HTMLResponse)
19
+ async def index(request: Request):
20
+ return templates.TemplateResponse("index.html", {"request": request})
21
+
22
+ def _save_upload(dst_dir: str, name: str, file: Optional[StarletteUploadFile]) -> Optional[str]:
23
+ if not file:
24
+ return None
25
+ path = os.path.join(dst_dir, name)
26
+ with open(path, "wb") as f:
27
+ shutil.copyfileobj(file.file, f)
28
+ return path
29
+
30
+ @app.post("/create")
31
+ async def create(
32
+ request: Request,
33
+ preset: str = Form(...),
34
+ music: Optional[UploadFile] = File(None),
35
+ video: Optional[UploadFile] = File(None),
36
+ logo: Optional[UploadFile] = File(None),
37
+ ):
38
+ job_id = str(uuid.uuid4())[:8]
39
+ job_dir = os.path.join(JOBS_DIR, job_id)
40
+ os.makedirs(job_dir, exist_ok=True)
41
+
42
+ # save files if provided
43
+ saved = {}
44
+ if music: saved["music"] = _save_upload(job_dir, f"music_{music.filename}", music)
45
+ if video: saved["video"] = _save_upload(job_dir, f"video_{video.filename}", video)
46
+ if logo: saved["logo"] = _save_upload(job_dir, f"logo_{logo.filename}", logo)
47
+
48
+ # NOTE: این نسخه‌ی ساده فقط فایل‌ها را ذخیره می‌کند تا Space بالا بیاید.
49
+ # بعداً می‌توانیم مرحله‌های پردازش ویدیو (librosa/moviepy) را اضافه کنیم.
50
+
51
+ return templates.TemplateResponse(
52
+ "done.html",
53
+ {
54
+ "request": request,
55
+ "job_id": job_id,
56
+ "preset": preset,
57
+ "saved": saved,
58
+ },
59
+ )
app/static/style.css ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root { --bg:#0a0b0e; --panel:#12141a; --text:#e7e9ee; --muted:#9aa3b2; --accent:#7c5cff; }
2
+ *{box-sizing:border-box;font-family:system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial}
3
+ body{margin:0;background:var(--bg);color:var(--text)}
4
+ .container{max-width:820px;margin:40px auto;padding:0 16px}
5
+ h1{font-weight:800;letter-spacing:-.3px;margin:0 0 12px}
6
+ .muted{color:var(--muted)}
7
+ .tiny{font-size:12px}
8
+ .card{background:var(--panel);border:1px solid #222533;border-radius:14px;padding:18px;margin:16px 0}
9
+ fieldset{border:1px dashed #2a2e3a;border-radius:12px;padding:12px 14px;margin:12px 0}
10
+ legend{padding:0 6px;color:var(--muted);font-size:12px}
11
+ label{display:block;margin:10px 0}
12
+ .radio{display:block;margin:8px 0}
13
+ button.primary, a.primary{display:inline-block;background:var(--accent);color:white;border:none;border-radius:10px;padding:10px 16px;text-decoration:none}
14
+ code{background:#1a1f2b;padding:2px 6px;border-radius:6px}
app/templates/done.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>FlexCut — نتیجه</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <h1>درخواست ثبت شد ✅</h1>
12
+ <p>شناسه‌ی Job شما: <b>{{ job_id }}</b></p>
13
+ <p>Preset انتخابی: <code>{{ preset }}</code></p>
14
+ <div class="card">
15
+ <h3>فایل‌های ذخیره شده</h3>
16
+ {% if saved %}
17
+ <ul>
18
+ {% for k, v in saved.items() %}
19
+ <li><b>{{ k }}</b>: <code>{{ v }}</code></li>
20
+ {% endfor %}
21
+ </ul>
22
+ {% else %}
23
+ <p class="muted">فایلی آپلود نشده است.</p>
24
+ {% endif %}
25
+ </div>
26
+ <p><a href="/" class="primary">بازگشت</a></p>
27
+ </div>
28
+ </body>
29
+ </html>
app/templates/index.html ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>FlexCut — تدوین هوشمند ویدیو (وب)</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <h1>FlexCut — تدوین هوشمند ویدیو (تحت وب)</h1>
12
+ <p class="muted">لطفاً preset را انتخاب کنید و فایل‌ها را آپلود کنید. سپس روی «ساخت ویدیو» بزنید.</p>
13
+
14
+ <form method="post" action="/create" enctype="multipart/form-data" class="card">
15
+ <fieldset>
16
+ <legend>۱) انتخاب Preset</legend>
17
+ <label class="radio">
18
+ <input type="radio" name="preset" value="reels_fast_9x16" checked>
19
+ Reels Fast 20s (9:16)
20
+ </label>
21
+ <label class="radio">
22
+ <input type="radio" name="preset" value="showreel_16x9">
23
+ Showreel 45s (16:9)
24
+ </label>
25
+ <label class="radio">
26
+ <input type="radio" name="preset" value="dual_export">
27
+ Dual Export (9:16 + 16:9)
28
+ </label>
29
+ </fieldset>
30
+
31
+ <fieldset>
32
+ <legend>۲) آپلود فایل‌ها</legend>
33
+ <label>موزیک (mp3/wav): <input type="file" name="music" accept=".mp3,.wav" /></label>
34
+ <label>ویدیو (mp4/mov): <input type="file" name="video" accept=".mp4,.mov" /></label>
35
+ <label>لوگو (PNG، اختیاری): <input type="file" name="logo" accept=".png" /></label>
36
+ </fieldset>
37
+
38
+ <button type="submit" class="primary">ساخت ویدیو</button>
39
+ </form>
40
+
41
+ <p class="tiny muted">این نسخه فقط فایل‌ها را ذخیره می‌کند تا مطمئن شویم Space به‌درستی بالا می‌آید. سپس منطق تدوین را اضافه می‌کنیم.</p>
42
+ </div>
43
+ </body>
44
+ </html>
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.112.2
2
+ uvicorn[standard]==0.30.6
3
+ jinja2==3.1.4
4
+ aiofiles==23.2.1
5
+ python-multipart==0.0.9
6
+ moviepy==1.0.3
7
+ librosa==0.10.2.post1
8
+ scenedetect==0.6.4 # optional; we don't import it by default
9
+ numpy<2.0 # keep under 2 for best compat with moviepy/librosa