HuuDatLego commited on
Commit
6376ca1
·
verified ·
1 Parent(s): 036b370

Upload folder using huggingface_hub

Browse files
Files changed (10) hide show
  1. .gitignore +1 -0
  2. local_setup_guide.md +48 -0
  3. main.py +47 -5
  4. pyproject.toml +2 -0
  5. services/ai_pipeline.py +30 -0
  6. static/styles.css +52 -0
  7. templates/index.html +306 -0
  8. templates/tts.html +308 -0
  9. uv.lock +15 -7
  10. worker.py +48 -1
.gitignore CHANGED
@@ -161,3 +161,4 @@ tmp/
161
  temp/
162
  outputs/
163
  *.pen
 
 
161
  temp/
162
  outputs/
163
  *.pen
164
+ uv_tree.txt
local_setup_guide.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hướng dẫn Chạy Local - UI-VieNeu Backend
2
+
3
+ Tài liệu này ghi lại toàn bộ các bước để thiết lập và chạy dự án Video Subtitle & AI Voiceover ở môi trường máy tính cá nhân (Windows).
4
+
5
+ ## 1. Cài đặt Công cụ Quản lý (uv)
6
+ Nếu máy bạn chưa có `uv`, hãy mở PowerShell và chạy lệnh sau:
7
+ ```powershell
8
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
9
+ ```
10
+
11
+ ## 2. Thiết lập Môi trường và Thư viện
12
+ Di chuyển vào thư mục dự án và cài bộ thư viện:
13
+ ```powershell
14
+ # Cài đặt toàn bộ thư viện từ pyproject.toml vào thư mục .venv
15
+ uv sync
16
+ ```
17
+
18
+ ## 3. Khởi động Máy chủ Redis (Memurai)
19
+ Dự án cần Redis để làm "bưu điện" chuyển tin nhắn cho Celery.
20
+ - **Bước 1:** Mở **VSCode bằng quyền Administrator**.
21
+ - **Bước 2:** Chạy lệnh bật dịch vụ:
22
+ ```powershell
23
+ Start-Service Memurai
24
+ ```
25
+ *(Kiểm tra màu xanh trong RedisInsight để chắc chắn đã bật thành công)*
26
+
27
+ ## 4. Chạy Hệ thống (Cần mở 2 Terminal song song)
28
+
29
+ ### Terminal 1: Chạy Celery Worker (Xử lý AI & FFmpeg)
30
+ Sử dụng `uv run` để đảm bảo dùng đúng thư viện trong môi trường ảo:
31
+ ```powershell
32
+ uv run celery -A worker worker --loglevel=info -P solo
33
+ ```
34
+ > **Lưu ý:** Tham số `-P solo` là bắt buộc để Celery có thể chạy được trên hệ điều hành Windows.
35
+
36
+ ### Terminal 2: Chạy FastAPI Server (Cổng kết nối API)
37
+ ```powershell
38
+ uv run uvicorn main:app --reload
39
+ ```
40
+ - API sẽ chạy tại: `http://127.0.0.1:8000`
41
+ - Tài liệu API (Swagger UI): `http://127.0.0.1:8000/docs`
42
+
43
+ ---
44
+
45
+ ## Các lỗi thường gặp và cách xử lý
46
+ 1. **ModuleNotFoundError:** Hãy chắc chắn bạn luôn có chữ `uv run` ở đầu lệnh để nó nhận diện được thư viện trong `.venv`.
47
+ 2. **ConnectionError (Redis):** Kiểm tra xem Memurai đã được Start chưa.
48
+ 3. **Permission Denied:** Luôn chạy Terminal/VSCode với quyền Administrator khi can thiệp vào các Service như Memurai.
main.py CHANGED
@@ -1,11 +1,20 @@
1
  import os
2
- from fastapi import FastAPI, UploadFile, File, Form
 
 
 
 
 
3
  from pydantic import BaseModel
4
  from supabase import create_client, Client
5
- from worker import render_video_task
6
 
7
  app = FastAPI(title="VieNeu Video AI processing API")
8
 
 
 
 
 
9
  # Setup Supabase
10
  SUPABASE_URL = os.getenv("SUPABASE_URL", "https://your-project.supabase.co")
11
  SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "your-service-key")
@@ -15,9 +24,13 @@ class RenderJobRequest(BaseModel):
15
  script_text: str
16
  voice_preset_id: str = "default"
17
 
18
- @app.get("/")
19
- def read_root():
20
- return {"status": "online", "message": "Video Processing API is running."}
 
 
 
 
21
 
22
  @app.post("/api/v1/jobs/submit")
23
  async def submit_job(
@@ -53,6 +66,35 @@ async def submit_job(
53
 
54
  return {"job_id": job_id, "status": "processing_queued"}
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  @app.get("/api/v1/jobs/{job_id}")
57
  async def get_job_status(job_id: str):
58
  response = supabase.table("video_jobs").select("*").eq("id", job_id).execute()
 
1
  import os
2
+ from dotenv import load_dotenv
3
+ load_dotenv(override=True)
4
+ from fastapi import FastAPI, UploadFile, File, Form, Request
5
+ from fastapi.responses import HTMLResponse
6
+ from fastapi.staticfiles import StaticFiles
7
+ from fastapi.templating import Jinja2Templates
8
  from pydantic import BaseModel
9
  from supabase import create_client, Client
10
+ from worker import render_video_task, generate_tts_task
11
 
12
  app = FastAPI(title="VieNeu Video AI processing API")
13
 
14
+ # Mount thư mục tĩnh và giao diện HTML
15
+ app.mount("/static", StaticFiles(directory="static"), name="static")
16
+ templates = Jinja2Templates(directory="templates")
17
+
18
  # Setup Supabase
19
  SUPABASE_URL = os.getenv("SUPABASE_URL", "https://your-project.supabase.co")
20
  SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "your-service-key")
 
24
  script_text: str
25
  voice_preset_id: str = "default"
26
 
27
+ @app.get("/", response_class=HTMLResponse)
28
+ async def read_root(request: Request):
29
+ return templates.TemplateResponse(request=request, name="index.html")
30
+
31
+ @app.get("/tts", response_class=HTMLResponse)
32
+ async def read_tts(request: Request):
33
+ return templates.TemplateResponse(request=request, name="tts.html")
34
 
35
  @app.post("/api/v1/jobs/submit")
36
  async def submit_job(
 
66
 
67
  return {"job_id": job_id, "status": "processing_queued"}
68
 
69
+ @app.post("/api/v1/tts/generate")
70
+ async def submit_tts_job(
71
+ script: str = Form(...),
72
+ temperature: float = Form(0.5),
73
+ voice_preset: str = Form("default"),
74
+ ref_audio: UploadFile = File(None)
75
+ ):
76
+ """
77
+ Submits a pure Text-To-Speech task to Celery.
78
+ """
79
+ ref_audio_path = None
80
+ if ref_audio:
81
+ ref_audio_bytes = await ref_audio.read()
82
+ ref_audio_path = f"references/{ref_audio.filename}"
83
+ supabase.storage.from_("content").upload(path=ref_audio_path, file=ref_audio_bytes)
84
+
85
+ # Note: Using generic "video_jobs" table to track TTS jobs as well to save setup time.
86
+ db_resp = supabase.table("video_jobs").insert({
87
+ "status": "pending",
88
+ "script": script,
89
+ "raw_video_path": "audio_only"
90
+ }).execute()
91
+
92
+ job_id = db_resp.data[0]["id"] if db_resp.data else "unknown"
93
+
94
+ generate_tts_task.delay(job_id, script, voice_preset, temperature, ref_audio_path)
95
+
96
+ return {"job_id": job_id, "status": "processing_queued"}
97
+
98
  @app.get("/api/v1/jobs/{job_id}")
99
  async def get_job_status(job_id: str):
100
  response = supabase.table("video_jobs").select("*").eq("id", job_id).execute()
pyproject.toml CHANGED
@@ -9,7 +9,9 @@ dependencies = [
9
  "fastapi>=0.136.0",
10
  "faster-whisper>=1.2.1",
11
  "ffmpeg-python>=0.2.0",
 
12
  "pydantic>=2.13.2",
 
13
  "python-multipart>=0.0.26",
14
  "redis>=7.4.0",
15
  "supabase>=2.28.3",
 
9
  "fastapi>=0.136.0",
10
  "faster-whisper>=1.2.1",
11
  "ffmpeg-python>=0.2.0",
12
+ "jinja2>=3.1.6",
13
  "pydantic>=2.13.2",
14
+ "python-dotenv>=1.2.2",
15
  "python-multipart>=0.0.26",
16
  "redis>=7.4.0",
17
  "supabase>=2.28.3",
services/ai_pipeline.py CHANGED
@@ -67,6 +67,36 @@ def process_video_pipeline(tmpdir: str, video_file: str, script: str, ref_audio:
67
 
68
  return output_video
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  def generate_ass_file(words_data: list, dest_path: str):
71
  """
72
  Crafts an advanced SubStation Alpha file for Karaoke effects.
 
67
 
68
  return output_video
69
 
70
+ def generate_tts_only(tmpdir: str, script: str, ref_audio: str = None, temperature: float = 0.5) -> str:
71
+ """
72
+ Standalone function to just generate TTS audio.
73
+ """
74
+ tts_audio_path = os.path.join(tmpdir, "tts_voiceover.wav")
75
+
76
+ # Passing keyword arguments; if underlying model doesn't strictly accept temperature,
77
+ # python handles **kwargs flexibly if written cleanly in wrappers.
78
+ # To avoid crashing, we'll try to pass it to `infer`. If Vieneu object restricts kwargs tightly,
79
+ # we can trap the type error and fallback to not using temperature.
80
+
81
+ try:
82
+ if ref_audio:
83
+ my_voice = tts.encode_reference(ref_audio)
84
+ audio_array = tts.infer(text=script, voice=my_voice, temperature=temperature)
85
+ else:
86
+ # We assume default voices can be tuned with temperature
87
+ audio_array = tts.infer(text=script, temperature=temperature)
88
+ except TypeError:
89
+ # Fallback if Vieneu.infer doesn't support 'temperature'
90
+ print("Warning: Vieneu.infer doesn't support temperature. Ignoring it.")
91
+ if ref_audio:
92
+ my_voice = tts.encode_reference(ref_audio)
93
+ audio_array = tts.infer(text=script, voice=my_voice)
94
+ else:
95
+ audio_array = tts.infer(text=script)
96
+
97
+ tts.save(audio_array, tts_audio_path)
98
+ return tts_audio_path
99
+
100
  def generate_ass_file(words_data: list, dest_path: str):
101
  """
102
  Crafts an advanced SubStation Alpha file for Karaoke effects.
static/styles.css ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
2
+
3
+ body {
4
+ font-family: 'Inter', sans-serif;
5
+ }
6
+
7
+ .glass-panel {
8
+ background: rgba(31, 41, 55, 0.7);
9
+ backdrop-filter: blur(16px);
10
+ -webkit-backdrop-filter: blur(16px);
11
+ border: 1px solid rgba(255, 255, 255, 0.1);
12
+ }
13
+
14
+ .custom-bg {
15
+ background-image:
16
+ radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
17
+ radial-gradient(at 100% 0%, rgba(168, 85, 247, 0.15) 0px, transparent 50%),
18
+ radial-gradient(at 100% 100%, rgba(236, 72, 153, 0.15) 0px, transparent 50%),
19
+ radial-gradient(at 0% 100%, rgba(59, 130, 246, 0.15) 0px, transparent 50%);
20
+ background-color: #0f172a;
21
+ }
22
+
23
+ /* Custom Scrollbar */
24
+ ::-webkit-scrollbar {
25
+ width: 8px;
26
+ }
27
+ ::-webkit-scrollbar-track {
28
+ background: #1e293b;
29
+ }
30
+ ::-webkit-scrollbar-thumb {
31
+ background: #475569;
32
+ border-radius: 4px;
33
+ }
34
+ ::-webkit-scrollbar-thumb:hover {
35
+ background: #64748b;
36
+ }
37
+
38
+ /* Spinner Animation */
39
+ .spinner {
40
+ border: 3px solid rgba(255,255,255,0.1);
41
+ border-radius: 50%;
42
+ border-top: 3px solid #8b5cf6;
43
+ width: 24px;
44
+ height: 24px;
45
+ -webkit-animation: spin 1s linear infinite; /* Safari */
46
+ animation: spin 1s linear infinite;
47
+ }
48
+
49
+ @keyframes spin {
50
+ 0% { transform: rotate(0deg); }
51
+ 100% { transform: rotate(360deg); }
52
+ }
templates/index.html ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>UI-VieNeu | AI Video Engine</title>
7
+ <!-- Tailwind CSS CDN -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: '#8b5cf6',
16
+ secondary: '#ec4899'
17
+ }
18
+ }
19
+ }
20
+ }
21
+ </script>
22
+ <!-- Custom Styles -->
23
+ <link rel="stylesheet" href="/static/styles.css">
24
+ <!-- Feather Icons -->
25
+ <script src="https://unpkg.com/feather-icons"></script>
26
+ </head>
27
+ <body class="text-slate-200 min-h-screen custom-bg selection:bg-primary selection:text-white">
28
+
29
+ <!-- Header -->
30
+ <header class="border-b border-slate-700/50 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
31
+ <div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
32
+ <div class="flex items-center gap-3">
33
+ <div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-violet-600 to-fuchsia-600 flex items-center justify-center shadow-lg shadow-primary/20">
34
+ <i data-feather="video" class="text-white w-5 h-5"></i>
35
+ </div>
36
+ <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">
37
+ UI-VieNeu <span class="font-normal text-sm text-primary ml-1">v1.0</span>
38
+ </h1>
39
+ </div>
40
+ <a href="/docs" target="_blank" class="text-sm text-slate-400 hover:text-white transition-colors flex items-center gap-2">
41
+ <i data-feather="code" class="w-4 h-4"></i> API Docs
42
+ </a>
43
+ </div>
44
+ </header>
45
+
46
+ <!-- Main Content -->
47
+ <main class="max-w-6xl mx-auto px-6 py-10 grid grid-cols-1 lg:grid-cols-12 gap-8">
48
+
49
+ <!-- Left Column: Input Form -->
50
+ <div class="lg:col-span-5 space-y-6">
51
+ <div class="glass-panel rounded-2xl p-6 shadow-xl">
52
+ <h2 class="text-lg font-semibold mb-6 flex items-center gap-2 text-white">
53
+ <i data-feather="edit-3" class="w-5 h-5 text-primary"></i> Create New Video
54
+ </h2>
55
+
56
+ <form id="uploadForm" class="space-y-5">
57
+
58
+ <!-- Video Upload -->
59
+ <div>
60
+ <label class="block text-sm font-medium text-slate-300 mb-2">Raw Video (Background)</label>
61
+ <div class="relative group">
62
+ <input type="file" id="videoFile" accept="video/mp4,video/x-m4v,video/*" required
63
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
64
+ <div class="border-2 border-dashed border-slate-600 rounded-xl p-4 flex items-center justify-center gap-3 bg-slate-800/50 group-hover:bg-slate-800 transition-colors group-hover:border-primary/50">
65
+ <i data-feather="upload-cloud" class="w-6 h-6 text-slate-400 group-hover:text-primary transition-colors"></i>
66
+ <span id="videoFileName" class="text-sm text-slate-400">Select MP4 file...</span>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Voice Reference (Optional) -->
72
+ <div>
73
+ <label class="block text-sm font-medium text-slate-300 mb-2">Voice Reference Audio <span class="text-xs text-slate-500 font-normal">(Optional for cloning)</span></label>
74
+ <div class="relative group">
75
+ <input type="file" id="audioFile" accept="audio/*"
76
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
77
+ <div class="border border-slate-700 rounded-xl p-3 flex items-center gap-3 bg-slate-800/30 group-hover:bg-slate-800/80 transition-colors">
78
+ <i data-feather="mic" class="w-5 h-5 text-slate-500 group-hover:text-secondary transition-colors"></i>
79
+ <span id="audioFileName" class="text-sm text-slate-500">Select audio file...</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Script Input -->
85
+ <div>
86
+ <label class="block text-sm font-medium text-slate-300 mb-2">Voiceover Script & Subtitles</label>
87
+ <textarea id="scriptText" rows="6" required placeholder="Xin chào các bạn, hôm nay mình sẽ hướng dẫn..."
88
+ class="w-full bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl p-4 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all placeholder:text-slate-600 resize-none font-medium"></textarea>
89
+ </div>
90
+
91
+ <!-- Submit Button -->
92
+ <button type="submit" id="submitBtn" class="w-full relative overflow-hidden group bg-white text-slate-900 font-semibold py-3.5 px-4 rounded-xl transition-all hover:scale-[1.02] hover:shadow-lg hover:shadow-white/10 flex items-center justify-center gap-2">
93
+ <span class="z-10 flex items-center gap-2">
94
+ <i data-feather="zap" class="w-5 h-5"></i> Generate Video
95
+ </span>
96
+ <div class="absolute inset-0 bg-gradient-to-r from-white to-slate-200 opacity-0 group-hover:opacity-100 transition-opacity"></div>
97
+ </button>
98
+
99
+ <p id="formError" class="text-red-400 text-sm font-medium hidden mt-2 text-center"></p>
100
+
101
+ </form>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Right Column: Result & Tracker -->
106
+ <div class="lg:col-span-7">
107
+ <div class="glass-panel rounded-2xl h-full shadow-xl flex flex-col overflow-hidden relative">
108
+
109
+ <!-- Status Bar -->
110
+ <div class="bg-slate-800/80 p-4 border-b border-slate-700 flex flex-wrap items-center justify-between gap-4">
111
+ <div class="flex items-center gap-3">
112
+ <div id="statusDot" class="w-3 h-3 rounded-full bg-slate-600"></div>
113
+ <span id="statusText" class="text-sm font-medium text-slate-300">Idle - Ready to render</span>
114
+ </div>
115
+ <div id="jobIdContainer" class="hidden">
116
+ <span class="text-xs font-mono text-slate-500 bg-slate-900 px-2 py-1 rounded-md border border-slate-700" id="jobIdLabel"></span>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Preview Area -->
121
+ <div class="flex-1 min-h-[400px] flex items-center justify-center p-6 bg-slate-900/30 relative">
122
+
123
+ <!-- Empty State -->
124
+ <div id="emptyState" class="text-center text-slate-500">
125
+ <div class="w-20 h-20 mx-auto bg-slate-800 rounded-full flex items-center justify-center mb-4 border border-slate-700">
126
+ <i data-feather="film" class="w-8 h-8 text-slate-600"></i>
127
+ </div>
128
+ <p class="font-medium">Your generated video will appear here</p>
129
+ </div>
130
+
131
+ <!-- Loading State -->
132
+ <div id="loadingState" class="hidden flex-col items-center justify-center absolute inset-0 bg-slate-900/80 backdrop-blur-sm z-10 transition-all duration-300">
133
+ <div class="spinner mb-4 border-t-primary"></div>
134
+ <h3 class="font-semibold text-lg text-white mb-1">Rendering in progress...</h3>
135
+ <p class="text-sm text-slate-400">This might take a while depending on server capacity.</p>
136
+ </div>
137
+
138
+ <!-- Result Output -->
139
+ <div id="resultState" class="hidden w-full h-full flex flex-col items-center justify-center">
140
+ <video id="resultVideo" controls class="max-w-full max-h-[500px] rounded-lg shadow-2xl border border-slate-700 bg-black"></video>
141
+ <div class="mt-6 flex gap-4">
142
+ <a id="downloadBtn" href="#" target="_blank" class="px-5 py-2 rounded-lg bg-primary/10 text-primary border border-primary/30 hover:bg-primary/20 transition-colors font-medium flex items-center gap-2">
143
+ <i data-feather="download" class="w-4 h-4"></i> Download Result
144
+ </a>
145
+ </div>
146
+ </div>
147
+
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ </main>
153
+
154
+ <script>
155
+ // Khởi tạo icons
156
+ feather.replace();
157
+
158
+ // Xử lý hiển thị tên file
159
+ document.getElementById('videoFile').addEventListener('change', function(e) {
160
+ const fileName = e.target.files[0] ? e.target.files[0].name : 'Select MP4 file...';
161
+ document.getElementById('videoFileName').textContent = fileName;
162
+ if(e.target.files[0]) document.getElementById('videoFileName').classList.remove('text-slate-400');
163
+ if(e.target.files[0]) document.getElementById('videoFileName').classList.add('text-white');
164
+ });
165
+
166
+ document.getElementById('audioFile').addEventListener('change', function(e) {
167
+ const fileName = e.target.files[0] ? e.target.files[0].name : 'Select audio file...';
168
+ document.getElementById('audioFileName').textContent = fileName;
169
+ });
170
+
171
+ // Xử lý Submit Form
172
+ const form = document.getElementById('uploadForm');
173
+ let pollInterval;
174
+
175
+ form.addEventListener('submit', async (e) => {
176
+ e.preventDefault();
177
+
178
+ const submitBtn = document.getElementById('submitBtn');
179
+ const errorTxt = document.getElementById('formError');
180
+ errorTxt.classList.add('hidden');
181
+
182
+ // Lấy dữ liệu
183
+ const videoFile = document.getElementById('videoFile').files[0];
184
+ const audioFile = document.getElementById('audioFile').files[0];
185
+ const scriptText = document.getElementById('scriptText').value;
186
+
187
+ // Đóng gói FormData
188
+ const formData = new FormData();
189
+ formData.append('script', scriptText);
190
+ formData.append('video', videoFile);
191
+ if (audioFile) formData.append('ref_audio', audioFile);
192
+
193
+ // Cập nhật UI loading Submit
194
+ submitBtn.disabled = true;
195
+ submitBtn.innerHTML = '<div class="spinner w-5 h-5 border-2 border-slate-900 border-t-transparent"></div> Uploading...';
196
+
197
+ // Ẩn states & Bật loading rendering
198
+ document.getElementById('emptyState').classList.add('hidden');
199
+ document.getElementById('resultState').classList.add('hidden');
200
+ document.getElementById('loadingState').classList.remove('hidden');
201
+ updateBadge('warning', 'Uploading files to server...');
202
+
203
+ try {
204
+ // Đẩy lên API
205
+ const response = await fetch('/api/v1/jobs/submit', {
206
+ method: 'POST',
207
+ body: formData
208
+ });
209
+
210
+ if (!response.ok) {
211
+ throw new Error(`Upload failed! Check your Supabase configuration or try again (Error ${response.status})`);
212
+ }
213
+
214
+ const data = await response.json();
215
+ const jobId = data.job_id;
216
+
217
+ // Hiển thị Job ID
218
+ document.getElementById('jobIdContainer').classList.remove('hidden');
219
+ document.getElementById('jobIdLabel').textContent = `ID: ${jobId}`;
220
+
221
+ updateBadge('processing', 'In Queue - Polling status...');
222
+
223
+ // Bắt đầu pollling check status định kỳ mỗi 3 giây
224
+ pollInterval = setInterval(() => checkJobStatus(jobId), 3000);
225
+
226
+ } catch (err) {
227
+ console.error(err);
228
+ errorTxt.textContent = err.message;
229
+ errorTxt.classList.remove('hidden');
230
+ document.getElementById('loadingState').classList.add('hidden');
231
+ document.getElementById('emptyState').classList.remove('hidden');
232
+ updateBadge('error', 'Error occurred');
233
+ } finally {
234
+ // Khôi phục nút
235
+ submitBtn.disabled = false;
236
+ submitBtn.innerHTML = `
237
+ <span class="z-10 flex items-center gap-2">
238
+ <i data-feather="zap" class="w-5 h-5"></i> Generate Another
239
+ </span>
240
+ <div class="absolute inset-0 bg-gradient-to-r from-white to-slate-200 opacity-0 group-hover:opacity-100 transition-opacity"></div>
241
+ `;
242
+ feather.replace();
243
+ }
244
+ });
245
+
246
+ // Hàm kiểm tra trạng thái tiến trình
247
+ async function checkJobStatus(jobId) {
248
+ try {
249
+ const res = await fetch(`/api/v1/jobs/${jobId}`);
250
+ if (!res.ok) return;
251
+
252
+ const jobData = await res.json();
253
+
254
+ if (jobData.status === 'processing') {
255
+ updateBadge('processing', 'Rendering Engine Running...');
256
+ }
257
+ else if (jobData.status === 'completed') {
258
+ clearInterval(pollInterval);
259
+ updateBadge('success', 'Completed Successfully!');
260
+ showResult(jobData.result_url);
261
+ }
262
+ else if (jobData.status === 'failed' || jobData.status === 'error') {
263
+ clearInterval(pollInterval);
264
+ updateBadge('error', `Failed: ${jobData.error || 'Unknown rendering error'}`);
265
+ document.getElementById('loadingState').classList.add('hidden');
266
+ document.getElementById('emptyState').classList.remove('hidden');
267
+ alert("Render Task Failed! Check Celery Worker logs.");
268
+ }
269
+
270
+ } catch (e) {
271
+ console.error("Polling error", e);
272
+ }
273
+ }
274
+
275
+ // Cập nhật thẻ trạng thái (Trắng, Vàng, Xanh, Đỏ)
276
+ function updateBadge(state, text) {
277
+ const dot = document.getElementById('statusDot');
278
+ const txt = document.getElementById('statusText');
279
+ txt.textContent = text;
280
+
281
+ // Reset class
282
+ dot.className = 'w-3 h-3 rounded-full';
283
+
284
+ if (state === 'idle') dot.classList.add('bg-slate-600');
285
+ if (state === 'warning') dot.classList.add('bg-yellow-500', 'animate-pulse');
286
+ if (state === 'processing') dot.classList.add('bg-blue-500', 'animate-pulse', 'shadow-[0_0_10px_rgba(59,130,246,0.6)]');
287
+ if (state === 'success') dot.classList.add('bg-green-500', 'shadow-[0_0_10px_rgba(34,197,94,0.6)]');
288
+ if (state === 'error') dot.classList.add('bg-red-500');
289
+ }
290
+
291
+ // Hiện kết quả
292
+ function showResult(videoUrl) {
293
+ document.getElementById('loadingState').classList.add('hidden');
294
+ document.getElementById('resultState').classList.remove('hidden');
295
+
296
+ const vid = document.getElementById('resultVideo');
297
+ vid.src = videoUrl;
298
+ vid.load();
299
+
300
+ const btn = document.getElementById('downloadBtn');
301
+ btn.href = videoUrl;
302
+ }
303
+
304
+ </script>
305
+ </body>
306
+ </html>
templates/tts.html ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Voice Studio | VieNeu AI</title>
7
+ <!-- Tailwind CSS CDN -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: '#8b5cf6',
16
+ secondary: '#ec4899',
17
+ dark: '#0f172a',
18
+ card: 'rgba(30, 41, 59, 0.7)'
19
+ }
20
+ }
21
+ }
22
+ }
23
+ </script>
24
+ <link rel="stylesheet" href="/static/styles.css">
25
+ <script src="https://unpkg.com/feather-icons"></script>
26
+ </head>
27
+ <body class="text-slate-200 min-h-screen custom-bg selection:bg-primary selection:text-white font-inter">
28
+
29
+ <!-- Header -->
30
+ <header class="border-b border-slate-700/50 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
31
+ <div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
32
+ <div class="flex items-center gap-3">
33
+ <div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-primary/20">
34
+ <i data-feather="mic" class="text-white w-5 h-5"></i>
35
+ </div>
36
+ <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">
37
+ Voice Studio <span class="font-normal text-sm text-primary ml-1">v2-Turbo</span>
38
+ </h1>
39
+ </div>
40
+ <nav class="flex gap-4 text-sm font-medium">
41
+ <a href="/" class="text-slate-400 hover:text-white transition-colors">Video AI</a>
42
+ <a href="/tts" class="text-white border-b-2 border-primary pb-1">Text to Speech</a>
43
+ </nav>
44
+ </div>
45
+ </header>
46
+
47
+ <main class="max-w-6xl mx-auto px-6 py-10 grid grid-cols-1 lg:grid-cols-12 gap-8">
48
+
49
+ <!-- Left Column: Controls (8 cols) -->
50
+ <div class="lg:col-span-8 space-y-6">
51
+
52
+ <!-- Text Input -->
53
+ <div class="glass-panel rounded-2xl p-6 shadow-xl">
54
+ <h2 class="text-lg font-semibold mb-4 text-white flex items-center gap-2">
55
+ <i data-feather="file-text" class="w-5 h-5 text-primary"></i> Nội dung kịch bản
56
+ </h2>
57
+ <textarea id="scriptText" rows="6" placeholder="Nhập văn bản Tiếng Việt hoặc Tiếng Anh vào đây..."
58
+ class="w-full bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl p-4 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all placeholder:text-slate-600 resize-none font-medium text-lg leading-relaxed"></textarea>
59
+ </div>
60
+
61
+ <!-- Voice Settings -->
62
+ <div class="glass-panel rounded-2xl p-6 shadow-xl">
63
+
64
+ <!-- Custom Tabs -->
65
+ <div class="flex border-b border-slate-700 mb-6">
66
+ <button id="tabPreset" class="px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary flex gap-2 items-center transition-colors">
67
+ <i data-feather="users" class="w-4 h-4"></i> Giọng có sẵn
68
+ </button>
69
+ <button id="tabClone" class="px-6 py-3 text-sm font-semibold text-slate-400 hover:text-slate-200 flex gap-2 items-center transition-colors">
70
+ <i data-feather="copy" class="w-4 h-4"></i> Clone Giọng
71
+ </button>
72
+ </div>
73
+
74
+ <div id="presetContainer" class="space-y-6">
75
+ <!-- Dropdown Select -->
76
+ <div>
77
+ <label class="block text-sm font-medium text-slate-300 mb-2">Chọn nhân vật</label>
78
+ <div class="relative">
79
+ <select id="voicePreset" class="w-full appearance-none bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary outline-none cursor-pointer font-medium">
80
+ <option value="bich_ngoc">Bích Ngọc (Nữ - Miền Bắc)</option>
81
+ <option value="pham_tuyen">Phạm Tuyên (Nam - Miền Bắc)</option>
82
+ <option value="thuc_doan">Thục Đoan (Nữ - Miền Nam)</option>
83
+ <option value="xuan_vinh">Xuân Vĩnh (Nam - Miền Nam)</option>
84
+ </select>
85
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-400">
86
+ <i data-feather="chevron-down" class="w-4 h-4"></i>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div id="cloneContainer" class="hidden space-y-6">
93
+ <div>
94
+ <label class="block text-sm font-medium text-slate-300 mb-2">Upload file âm thanh giọng mẫu</label>
95
+ <div class="relative group">
96
+ <input type="file" id="cloneAudio" accept="audio/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
97
+ <div class="border-2 border-dashed border-slate-600 rounded-xl p-6 flex flex-col items-center justify-center gap-3 bg-slate-800/50 group-hover:bg-slate-800 transition-colors group-hover:border-primary/50">
98
+ <i data-feather="upload-cloud" class="w-8 h-8 text-slate-400 group-hover:text-primary transition-colors"></i>
99
+ <span id="cloneFileName" class="text-sm font-medium text-slate-400">Trích xuất giọng từ file (.mp3, .wav)</span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Advanced Settings: Temperature Slider -->
106
+ <div class="mt-8 pt-6 border-t border-slate-700/50">
107
+ <div class="flex justify-between items-center mb-2">
108
+ <label class="text-sm font-medium text-slate-300 flex items-center gap-2">
109
+ <i data-feather="thermometer" class="w-4 h-4 text-secondary"></i> Nhiệt độ (Temperature)
110
+ </label>
111
+ <span id="tempValue" class="text-xs font-mono bg-slate-800 px-2 py-1 rounded text-primary">0.5</span>
112
+ </div>
113
+ <input type="range" id="temperature" min="0.1" max="1.5" step="0.1" value="0.5"
114
+ class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary">
115
+ <p class="text-xs text-slate-500 mt-2">Thấp = Khô khan chuẩn xác | Cao = Cảm xúc sáng tạo hơn.</p>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Submit Button -->
120
+ <button id="submitBtn" class="w-full relative overflow-hidden group bg-gradient-to-r from-primary to-blue-600 text-white font-bold py-4 px-4 rounded-xl shadow-[0_0_20px_rgba(139,92,246,0.3)] hover:shadow-[0_0_30px_rgba(139,92,246,0.5)] transition-all flex items-center justify-center gap-2">
121
+ <span class="z-10 flex items-center gap-2 text-lg">
122
+ <i data-feather="zap" class="w-5 h-5"></i> Bắt đầu tổng hợp Audio
123
+ </span>
124
+ </button>
125
+
126
+ </div>
127
+
128
+ <!-- Right Column: Result (4 cols) -->
129
+ <div class="lg:col-span-4">
130
+ <div class="glass-panel flex flex-col rounded-2xl h-full shadow-xl sticky top-24">
131
+
132
+ <div class="p-6 border-b border-slate-700 flex items-center gap-2">
133
+ <i data-feather="headphones" class="w-5 h-5 text-secondary"></i>
134
+ <h3 class="font-semibold text-white">Audio Kết Quả</h3>
135
+ </div>
136
+
137
+ <div class="flex-1 p-6 flex flex-col items-center justify-center min-h-[300px] relative">
138
+
139
+ <!-- Empty State -->
140
+ <div id="emptyResult" class="text-center">
141
+ <i data-feather="music" class="w-12 h-12 text-slate-700 mx-auto mb-4"></i>
142
+ <p class="text-slate-500 text-sm font-medium">Bấm "Bắt đầu" để tạo âm thanh</p>
143
+ </div>
144
+
145
+ <!-- Loading State -->
146
+ <div id="loadingResult" class="hidden flex-col items-center">
147
+ <div class="spinner w-8 h-8 border-[3px] border-t-primary mb-4"></div>
148
+ <p class="text-primary font-medium animate-pulse">Đang tổng hợp...</p>
149
+ <p class="text-xs text-slate-500 mt-1">Sẽ mất vài giây để xử lý</p>
150
+ </div>
151
+
152
+ <!-- Audio Player Area -->
153
+ <div id="audioResult" class="hidden w-full flex flex-col items-center space-y-6">
154
+ <div class="w-20 h-20 bg-gradient-to-tr from-primary to-secondary rounded-full flex items-center justify-center shadow-[0_0_40px_rgba(236,72,153,0.3)] beep-anim">
155
+ <i data-feather="play" class="w-8 h-8 text-white ml-1"></i>
156
+ </div>
157
+ <audio id="player" controls class="w-full"></audio>
158
+ <a id="downloadBtn" href="#" download="vieneu_output.wav" class="text-sm font-medium text-slate-300 hover:text-white flex items-center gap-1 bg-slate-800 py-2 px-4 rounded-lg border border-slate-700 hover:border-slate-500 transition-colors">
159
+ <i data-feather="download" class="w-4 h-4"></i> Tải file .wav
160
+ </a>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </main>
166
+
167
+ <style>
168
+ .beep-anim {
169
+ animation: pulse-glow 2s infinite;
170
+ }
171
+ @keyframes pulse-glow {
172
+ 0% { box-shadow: 0 0 0 0 rgba(236,72,153, 0.4); }
173
+ 70% { box-shadow: 0 0 0 20px rgba(236,72,153, 0); }
174
+ 100% { box-shadow: 0 0 0 0 rgba(236,72,153, 0); }
175
+ }
176
+ </style>
177
+
178
+ <script>
179
+ feather.replace();
180
+
181
+ // Range Slider Value update
182
+ const tempSlider = document.getElementById('temperature');
183
+ const tempValue = document.getElementById('tempValue');
184
+ tempSlider.addEventListener('input', (e) => {
185
+ tempValue.textContent = parseFloat(e.target.value).toFixed(1);
186
+ });
187
+
188
+ // Tabs Logic
189
+ const tabPreset = document.getElementById('tabPreset');
190
+ const tabClone = document.getElementById('tabClone');
191
+ const presetContainer = document.getElementById('presetContainer');
192
+ const cloneContainer = document.getElementById('cloneContainer');
193
+ let isCloning = false;
194
+
195
+ tabPreset.addEventListener('click', () => {
196
+ isCloning = false;
197
+ tabPreset.className = "px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary flex gap-2 items-center transition-colors";
198
+ tabClone.className = "px-6 py-3 text-sm font-semibold text-slate-400 hover:text-slate-200 flex gap-2 items-center transition-colors border-b-2 border-transparent";
199
+ presetContainer.classList.remove('hidden');
200
+ cloneContainer.classList.add('hidden');
201
+ });
202
+
203
+ tabClone.addEventListener('click', () => {
204
+ isCloning = true;
205
+ tabClone.className = "px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary flex gap-2 items-center transition-colors";
206
+ tabPreset.className = "px-6 py-3 text-sm font-semibold text-slate-400 hover:text-slate-200 flex gap-2 items-center transition-colors border-b-2 border-transparent";
207
+ presetContainer.classList.add('hidden');
208
+ cloneContainer.classList.remove('hidden');
209
+ });
210
+
211
+ document.getElementById('cloneAudio').addEventListener('change', function(e) {
212
+ const fileName = e.target.files[0] ? e.target.files[0].name : 'Trích xuất giọng từ file (.mp3, .wav)';
213
+ document.getElementById('cloneFileName').textContent = fileName;
214
+ });
215
+
216
+ // Submit logic
217
+ const submitBtn = document.getElementById('submitBtn');
218
+ let pollInterval;
219
+
220
+ submitBtn.addEventListener('click', async () => {
221
+ const text = document.getElementById('scriptText').value;
222
+ if(!text.trim()) {
223
+ alert("Vui lòng nhập kịch bản cần đọc!");
224
+ return;
225
+ }
226
+
227
+ const formData = new FormData();
228
+ formData.append('script', text);
229
+ formData.append('temperature', parseFloat(tempSlider.value));
230
+
231
+ if (isCloning) {
232
+ const cloneAudio = document.getElementById('cloneAudio').files[0];
233
+ if(cloneAudio) formData.append('ref_audio', cloneAudio);
234
+ } else {
235
+ formData.append('voice_preset', document.getElementById('voicePreset').value);
236
+ }
237
+
238
+ // Update UI
239
+ submitBtn.disabled = true;
240
+ submitBtn.classList.add('opacity-70');
241
+ document.getElementById('emptyResult').classList.add('hidden');
242
+ document.getElementById('audioResult').classList.add('hidden');
243
+ document.getElementById('loadingResult').classList.remove('hidden');
244
+ document.getElementById('loadingResult').classList.add('flex');
245
+
246
+ try {
247
+ const response = await fetch('/api/v1/tts/generate', {
248
+ method: 'POST',
249
+ body: formData
250
+ });
251
+
252
+ if (!response.ok) throw new Error("Upload Failed");
253
+
254
+ const data = await response.json();
255
+ const jobId = data.job_id;
256
+
257
+ pollInterval = setInterval(() => checkTtsJobStatus(jobId), 1500); // Check faster for TTS
258
+ } catch (err) {
259
+ alert("Error submitting request: " + err);
260
+ resetSubmitUI();
261
+ }
262
+ });
263
+
264
+ async function checkTtsJobStatus(jobId) {
265
+ try {
266
+ const res = await fetch(`/api/v1/jobs/${jobId}`);
267
+ if (!res.ok) return;
268
+
269
+ const jobData = await res.json();
270
+
271
+ if (jobData.status === 'completed') {
272
+ clearInterval(pollInterval);
273
+ showResult(jobData.result_url);
274
+ resetSubmitUI();
275
+ } else if (jobData.status === 'failed' || jobData.status === 'error') {
276
+ clearInterval(pollInterval);
277
+ alert("Lỗi quá trình Render TTS!");
278
+ resetSubmitUI();
279
+ }
280
+ } catch(e) {}
281
+ }
282
+
283
+ function showResult(audioUrl) {
284
+ document.getElementById('loadingResult').classList.add('hidden');
285
+ document.getElementById('loadingResult').classList.remove('flex');
286
+ document.getElementById('audioResult').classList.remove('hidden');
287
+
288
+ const player = document.getElementById('player');
289
+ player.src = audioUrl;
290
+ player.play(); // Auto play when done!
291
+
292
+ const btn = document.getElementById('downloadBtn');
293
+ btn.href = audioUrl;
294
+ }
295
+
296
+ function resetSubmitUI() {
297
+ submitBtn.disabled = false;
298
+ submitBtn.classList.remove('opacity-70');
299
+ if (document.getElementById('loadingResult').classList.contains('flex')) {
300
+ document.getElementById('loadingResult').classList.add('hidden');
301
+ document.getElementById('loadingResult').classList.remove('flex');
302
+ document.getElementById('emptyResult').classList.remove('hidden');
303
+ }
304
+ }
305
+
306
+ </script>
307
+ </body>
308
+ </html>
uv.lock CHANGED
@@ -962,19 +962,14 @@ wheels = [
962
  [[package]]
963
  name = "llama-cpp-python"
964
  version = "0.3.16"
965
- source = { registry = "https://pnnbao97.github.io/llama-cpp-python-v0.3.16/cpu/" }
966
  dependencies = [
967
  { name = "diskcache" },
968
  { name = "jinja2" },
969
  { name = "numpy" },
970
  { name = "typing-extensions" },
971
  ]
972
- wheels = [
973
- { url = "https://github.com/pnnbao97/VieNeu-TTS/releases/download/wheels-v0.3.16/llama_cpp_python-0.3.16-cp311-cp311-win_amd64.whl" },
974
- { url = "https://github.com/pnnbao97/VieNeu-TTS/releases/download/wheels-v0.3.16/llama_cpp_python-0.3.16-cp312-cp312-win_amd64.whl" },
975
- { url = "https://github.com/pnnbao97/VieNeu-TTS/releases/download/wheels-v0.3.16/llama_cpp_python-0.3.16-cp313-cp313-win_amd64.whl" },
976
- { url = "https://github.com/pnnbao97/VieNeu-TTS/releases/download/wheels-v0.3.16/llama_cpp_python-0.3.16-cp314-cp314-win_amd64.whl" },
977
- ]
978
 
979
  [[package]]
980
  name = "llvmlite"
@@ -2209,6 +2204,15 @@ wheels = [
2209
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
2210
  ]
2211
 
 
 
 
 
 
 
 
 
 
2212
  [[package]]
2213
  name = "python-multipart"
2214
  version = "0.0.26"
@@ -2836,7 +2840,9 @@ dependencies = [
2836
  { name = "fastapi" },
2837
  { name = "faster-whisper" },
2838
  { name = "ffmpeg-python" },
 
2839
  { name = "pydantic" },
 
2840
  { name = "python-multipart" },
2841
  { name = "redis" },
2842
  { name = "supabase" },
@@ -2850,7 +2856,9 @@ requires-dist = [
2850
  { name = "fastapi", specifier = ">=0.136.0" },
2851
  { name = "faster-whisper", specifier = ">=1.2.1" },
2852
  { name = "ffmpeg-python", specifier = ">=0.2.0" },
 
2853
  { name = "pydantic", specifier = ">=2.13.2" },
 
2854
  { name = "python-multipart", specifier = ">=0.0.26" },
2855
  { name = "redis", specifier = ">=7.4.0" },
2856
  { name = "supabase", specifier = ">=2.28.3" },
 
962
  [[package]]
963
  name = "llama-cpp-python"
964
  version = "0.3.16"
965
+ source = { registry = "https://pypi.org/simple" }
966
  dependencies = [
967
  { name = "diskcache" },
968
  { name = "jinja2" },
969
  { name = "numpy" },
970
  { name = "typing-extensions" },
971
  ]
972
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/b4/c8cd17629ced0b9644a71d399a91145aedef109c0333443bef015e45b704/llama_cpp_python-0.3.16.tar.gz", hash = "sha256:34ed0f9bd9431af045bb63d9324ae620ad0536653740e9bb163a2e1fcb973be6", size = 50688636, upload-time = "2025-08-15T04:58:29.212Z" }
 
 
 
 
 
973
 
974
  [[package]]
975
  name = "llvmlite"
 
2204
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
2205
  ]
2206
 
2207
+ [[package]]
2208
+ name = "python-dotenv"
2209
+ version = "1.2.2"
2210
+ source = { registry = "https://pypi.org/simple" }
2211
+ sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
2212
+ wheels = [
2213
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
2214
+ ]
2215
+
2216
  [[package]]
2217
  name = "python-multipart"
2218
  version = "0.0.26"
 
2840
  { name = "fastapi" },
2841
  { name = "faster-whisper" },
2842
  { name = "ffmpeg-python" },
2843
+ { name = "jinja2" },
2844
  { name = "pydantic" },
2845
+ { name = "python-dotenv" },
2846
  { name = "python-multipart" },
2847
  { name = "redis" },
2848
  { name = "supabase" },
 
2856
  { name = "fastapi", specifier = ">=0.136.0" },
2857
  { name = "faster-whisper", specifier = ">=1.2.1" },
2858
  { name = "ffmpeg-python", specifier = ">=0.2.0" },
2859
+ { name = "jinja2", specifier = ">=3.1.6" },
2860
  { name = "pydantic", specifier = ">=2.13.2" },
2861
+ { name = "python-dotenv", specifier = ">=1.2.2" },
2862
  { name = "python-multipart", specifier = ">=0.0.26" },
2863
  { name = "redis", specifier = ">=7.4.0" },
2864
  { name = "supabase", specifier = ">=2.28.3" },
worker.py CHANGED
@@ -1,7 +1,10 @@
1
  import os
 
 
2
  from celery import Celery
3
  import tempfile
4
- from services.ai_pipeline import process_video_pipeline
 
5
 
6
  # Initialize Celery pointing to Redis
7
  celery_app = Celery(
@@ -47,3 +50,47 @@ def render_video_task(self, job_id: str, video_path: str, script: str, ref_audio
47
  except Exception as e:
48
  supabase.table("video_jobs").update({"status": "failed", "error": str(e)}).eq("id", job_id).execute()
49
  raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ from dotenv import load_dotenv
3
+ load_dotenv(override=True)
4
  from celery import Celery
5
  import tempfile
6
+ from services.ai_pipeline import process_video_pipeline, generate_tts_only
7
+ from supabase import create_client, Client
8
 
9
  # Initialize Celery pointing to Redis
10
  celery_app = Celery(
 
50
  except Exception as e:
51
  supabase.table("video_jobs").update({"status": "failed", "error": str(e)}).eq("id", job_id).execute()
52
  raise e
53
+
54
+ @celery_app.task
55
+ def generate_tts_task(job_id: str, script: str, voice: str, temperature: float, ref_audio_path: str = None):
56
+ # Setup Supabase client per worker
57
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
58
+ SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
59
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
60
+
61
+ supabase.table("video_jobs").update({"status": "processing"}).eq("id", job_id).execute()
62
+
63
+ try:
64
+ with tempfile.TemporaryDirectory() as tmpdir:
65
+ # 1. Download ref audio if it exists
66
+ local_ref_path = None
67
+ if ref_audio_path:
68
+ local_ref_path = os.path.join(tmpdir, "input_ref.wav")
69
+ with open(local_ref_path, 'wb') as f:
70
+ f.write(supabase.storage.from_("content").download(ref_audio_path))
71
+
72
+ # 2. Run Pure TTS Engine
73
+ result_audio_local = generate_tts_only(tmpdir, script, local_ref_path, temperature)
74
+
75
+ # 3. Upload Result Audio
76
+ final_audio_path = f"results/{job_id}.wav"
77
+ with open(result_audio_local, 'rb') as f:
78
+ supabase.storage.from_("content").upload(path=final_audio_path, file=f)
79
+
80
+ public_url = supabase.storage.from_("content").get_public_url(final_audio_path)
81
+
82
+ # 4. Mark job as complete
83
+ supabase.table("video_jobs").update({
84
+ "status": "completed",
85
+ "result_url": public_url
86
+ }).eq("id", job_id).execute()
87
+
88
+ except Exception as e:
89
+ import traceback
90
+ traceback.print_exc()
91
+
92
+ supabase.table("video_jobs").update({
93
+ "status": "error",
94
+ "error": str(e)
95
+ }).eq("id", job_id).execute()
96
+ raise e