HCPTangHY commited on
Commit
56283ad
·
verified ·
1 Parent(s): 6efdf0c

Upload 9 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Create a non-root user
5
+ RUN useradd -m -u 1000 user
6
+
7
+ # Switch to the new user
8
+ USER user
9
+
10
+ # Set home to the user's home directory and update PATH
11
+ ENV HOME=/home/user \
12
+ PATH=/home/user/.local/bin:$PATH
13
+
14
+ # Set the working directory
15
+ WORKDIR $HOME/app
16
+
17
+ # Install dependencies
18
+ COPY --chown=user backend/requirements.txt .
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy the application code
22
+ COPY --chown=user . .
23
+
24
+ # Set the working directory to the backend
25
+ WORKDIR $HOME/app/backend
26
+
27
+ # Expose the port the app runs on
28
+ EXPOSE 7860
29
+
30
+ # Run the application
31
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,20 @@
1
- ---
2
- title: Veo3
3
- emoji: 🏃
4
- colorFrom: green
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: VEO3 Video Generator
3
+ emoji: 📹
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+
10
+ # VEO3 Video Generator
11
+
12
+ This is a simple web application to generate videos using Google's VEO3 models via a Vertex Express Mode Key loophole.
13
+
14
+ ## How to use
15
+
16
+ 1. Enter your Vertex Express Key.
17
+ 2. Enter a text prompt.
18
+ 3. Optionally, upload an image or video.
19
+ 4. Select the desired parameters.
20
+ 5. Click "Generate Video".
backend/main.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
3
+ from fastapi.responses import FileResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+ from typing import Optional
6
+ import services.vertex_service as vertex_service
7
+ import services.video_service as video_service
8
+ import os
9
+
10
+ app = FastAPI()
11
+
12
+ @app.post("/api/generate_video")
13
+ async def generate_video(
14
+ api_key: str = Form(...),
15
+ prompt: str = Form(...),
16
+ negative_prompt: Optional[str] = Form(None),
17
+ model: str = Form(...),
18
+ aspect_ratio: str = Form(...),
19
+ duration: int = Form(...),
20
+ resolution: str = Form(...),
21
+ sample_count: int = Form(...),
22
+ seed: Optional[int] = Form(None),
23
+ person_generation: str = Form(...),
24
+ generate_audio: bool = Form(...),
25
+ enhance_prompt: bool = Form(...),
26
+ image: Optional[UploadFile] = File(None),
27
+ video: Optional[UploadFile] = File(None),
28
+ ):
29
+ try:
30
+ project_id = await vertex_service.get_project_id(api_key)
31
+
32
+ params = {
33
+ "prompt": prompt,
34
+ "negativePrompt": negative_prompt,
35
+ "model": model,
36
+ "aspectRatio": aspect_ratio,
37
+ "durationSeconds": duration,
38
+ "resolution": resolution,
39
+ "sampleCount": sample_count,
40
+ "seed": seed,
41
+ "personGeneration": person_generation,
42
+ "generateAudio": generate_audio,
43
+ "enhancePrompt": enhance_prompt,
44
+ }
45
+
46
+ if image:
47
+ params["image"] = await image.read()
48
+ params["image_mime_type"] = image.content_type
49
+
50
+ if video:
51
+ params["video"] = await video.read()
52
+ params["video_mime_type"] = video.content_type
53
+
54
+ model_id = params["model"]
55
+ operation_name = await vertex_service.start_video_generation(project_id, api_key, params)
56
+ video_data = await vertex_service.poll_video_status(project_id, model_id, operation_name, api_key)
57
+
58
+ video_path = await video_service.save_video(video_data)
59
+
60
+ return {"video_url": f"/api/video/{os.path.basename(video_path)}"}
61
+ except Exception as e:
62
+ raise HTTPException(status_code=400, detail=str(e))
63
+
64
+ @app.get("/api/video/{video_id}")
65
+ async def get_video(video_id: str):
66
+ video_path = os.path.join(video_service.VIDEO_DIR, video_id)
67
+ if os.path.exists(video_path):
68
+ return FileResponse(video_path)
69
+ raise HTTPException(status_code=404, detail="Video not found")
70
+
71
+ app.mount("/", StaticFiles(directory="../frontend", html=True), name="static")
72
+
73
+ if __name__ == "__main__":
74
+ uvicorn.run(app, host="0.0.0.0", port=8000)
backend/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ requests
5
+ aiohttp
6
+ python-dotenv
backend/services/vertex_service.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+ import base64
4
+ import re
5
+ import logging
6
+ from typing import Dict, Any
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+
10
+ # A simple in-memory cache for project IDs
11
+ project_id_cache = {}
12
+
13
+ async def get_project_id(api_key: str) -> str:
14
+ if api_key in project_id_cache:
15
+ return project_id_cache[api_key]
16
+
17
+ url = f"https://aiplatform.googleapis.com/v1/publishers/google/models/gemini-2.6:streamGenerateContent?key={api_key}"
18
+ headers = {"Content-Type": "application/json"}
19
+ data = {}
20
+
21
+ async with aiohttp.ClientSession() as session:
22
+ async with session.post(url, headers=headers, json=data) as response:
23
+ error_data = await response.json()
24
+ message = error_data[0]["error"]["message"]
25
+ match = re.search(r"projects/(\d+)/", message)
26
+ if match:
27
+ project_id = match.group(1)
28
+ project_id_cache[api_key] = project_id
29
+ return project_id
30
+ else:
31
+ raise Exception("Could not extract project ID")
32
+
33
+ async def start_video_generation(project_id: str, api_key: str, params: Dict[str, Any]) -> str:
34
+ location = "us-central1" # Or make this a parameter
35
+ model_id = params.pop("model")
36
+ url = f"https://{location}-aiplatform.googleapis.com/v1/projects/{project_id}/locations/{location}/publishers/google/models/{model_id}:predictLongRunning?key={api_key}"
37
+
38
+ instances = [{"prompt": params.get("prompt")}]
39
+ if "image" in params and params["image"]:
40
+ instances[0]["image"] = {
41
+ "bytesBase64Encoded": base64.b64encode(params["image"]).decode("utf-8"),
42
+ "mimeType": params["image_mime_type"]
43
+ }
44
+ if "video" in params and params["video"]:
45
+ instances[0]["video"] = {
46
+ "bytesBase64Encoded": base64.b64encode(params["video"]).decode("utf-8"),
47
+ "mimeType": params["video_mime_type"]
48
+ }
49
+
50
+ parameters = {
51
+ "aspectRatio": params.get("aspectRatio"),
52
+ "durationSeconds": params.get("durationSeconds"),
53
+ "resolution": params.get("resolution"),
54
+ "generateAudio": params.get("generateAudio"),
55
+ "enhancePrompt": params.get("enhancePrompt"),
56
+ "negativePrompt": params.get("negativePrompt"),
57
+ "personGeneration": params.get("personGeneration"),
58
+ "sampleCount": params.get("sampleCount"),
59
+ "seed": params.get("seed"),
60
+ }
61
+
62
+ # Remove None values from parameters
63
+ parameters = {k: v for k, v in parameters.items() if v is not None}
64
+
65
+ payload = {
66
+ "instances": instances,
67
+ "parameters": parameters
68
+ }
69
+
70
+ logging.info(f"Sending video generation request to {url} with payload: {payload}")
71
+ async with aiohttp.ClientSession() as session:
72
+ async with session.post(url, json=payload) as response:
73
+ data = await response.json()
74
+ logging.info(f"Received response: {data}")
75
+ response.raise_for_status()
76
+ return data["name"]
77
+
78
+ async def poll_video_status(project_id: str, model_id: str, operation_name: str, api_key: str) -> Dict[str, Any]:
79
+ location = "us-central1"
80
+ url = f"https://{location}-aiplatform.googleapis.com/v1/projects/{project_id}/locations/{location}/publishers/google/models/{model_id}:fetchPredictOperation?key={api_key}"
81
+
82
+ payload = {"operationName": operation_name}
83
+
84
+ async with aiohttp.ClientSession() as session:
85
+ while True:
86
+ logging.info(f"Polling status from {url} with payload: {payload}")
87
+ async with session.post(url, json=payload) as response:
88
+ data = await response.json()
89
+ logging.info(f"Received polling response: {data}")
90
+ response.raise_for_status()
91
+ if data.get("done"):
92
+ if "error" in data:
93
+ raise Exception(data['error']['message'])
94
+ if "response" in data and "raiMediaFilteredReasons" in data["response"]:
95
+ raise Exception(data['response']['raiMediaFilteredReasons'][0])
96
+ return data["response"]
97
+ await asyncio.sleep(5) # Poll every 5 seconds
backend/services/video_service.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import base64
4
+ import aiohttp
5
+ from typing import Dict, Any
6
+
7
+ VIDEO_DIR = "/tmp/generated_videos"
8
+
9
+ async def save_video(video_data: Dict[str, Any]) -> str:
10
+ os.makedirs(VIDEO_DIR, exist_ok=True)
11
+ video_filename = f"{uuid.uuid4()}.mp4"
12
+ video_path = os.path.join(VIDEO_DIR, video_filename)
13
+
14
+ if "gcsUri" in video_data["videos"][0]:
15
+ gcs_uri = video_data["videos"][0]["gcsUri"]
16
+ async with aiohttp.ClientSession() as session:
17
+ async with session.get(gcs_uri) as response:
18
+ response.raise_for_status()
19
+ with open(video_path, "wb") as f:
20
+ f.write(await response.read())
21
+ elif "bytesBase64Encoded" in video_data["videos"][0]:
22
+ video_bytes = base64.b64decode(video_data["videos"][0]["bytesBase64Encoded"])
23
+ with open(video_path, "wb") as f:
24
+ f.write(video_bytes)
25
+ else:
26
+ raise Exception("No video data found in response")
27
+
28
+ return video_path
frontend/app.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.getElementById("video-form").addEventListener("submit", async (event) => {
2
+ event.preventDefault();
3
+
4
+ const form = event.target;
5
+ const formData = new FormData();
6
+
7
+ formData.append("api_key", form["api-key"].value);
8
+ formData.append("prompt", form["prompt"].value);
9
+ formData.append("negative_prompt", form["negative-prompt"].value);
10
+ formData.append("model", form["model"].value);
11
+ formData.append("aspect_ratio", form["aspect-ratio"].value);
12
+ formData.append("duration", form["duration"].value);
13
+ formData.append("resolution", form["resolution"].value);
14
+ formData.append("sample_count", form["sample-count"].value);
15
+ if (form["seed"].value) {
16
+ formData.append("seed", form["seed"].value);
17
+ }
18
+ formData.append("person_generation", form["person-generation"].value);
19
+ formData.append("generate_audio", form["generate-audio"].checked);
20
+ formData.append("enhance_prompt", form["enhance-prompt"].checked);
21
+
22
+ const imageFile = form["image-prompt"].files[0];
23
+ if (imageFile) {
24
+ formData.append("image", imageFile);
25
+ }
26
+
27
+ const videoFile = form["video-prompt"].files[0];
28
+ if (videoFile) {
29
+ formData.append("video", videoFile);
30
+ }
31
+
32
+ const loader = document.getElementById("loader");
33
+ const videoElement = document.getElementById("generated-video");
34
+ const errorContainer = document.getElementById("error-container");
35
+
36
+ loader.classList.remove("hidden");
37
+ videoElement.style.display = "none";
38
+ errorContainer.classList.add("hidden");
39
+
40
+ try {
41
+ const response = await fetch("/api/generate_video", {
42
+ method: "POST",
43
+ body: formData,
44
+ });
45
+
46
+ if (!response.ok) {
47
+ const errorData = await response.json();
48
+ throw new Error(errorData.detail || "An unknown error occurred");
49
+ }
50
+
51
+ const data = await response.json();
52
+ videoElement.src = data.video_url;
53
+ videoElement.style.display = "block";
54
+ } catch (error) {
55
+ errorContainer.textContent = error.message;
56
+ errorContainer.classList.remove("hidden");
57
+ } finally {
58
+ loader.classList.add("hidden");
59
+ }
60
+ });
frontend/index.html ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VEO3 Video Generator</title>
7
+ <link rel="stylesheet" href="/style.css">
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <h1>VEO3 Video Generator</h1>
12
+ </header>
13
+ <main>
14
+ <form id="video-form">
15
+ <div class="form-grid">
16
+ <div class="form-group">
17
+ <label for="api-key">Vertex Express Key:</label>
18
+ <input type="text" id="api-key" name="api-key" required>
19
+ </div>
20
+ <div class="form-group">
21
+ <label for="model">Model:</label>
22
+ <select id="model" name="model">
23
+ <option value="veo-2.0-generate-001">veo-2.0-generate-001</option>
24
+ <option value="veo-3.0-generate-001">veo-3.0-generate-001</option>
25
+ <option value="veo-3.0-fast-generate-001">veo-3.0-fast-generate-001</option>
26
+ <option value="veo-3.0-generate-preview" selected>veo-3.0-generate-preview</option>
27
+ <option value="veo-3.0-fast-generate-preview">veo-3.0-fast-generate-preview</option>
28
+ </select>
29
+ </div>
30
+ <div class="form-group full-width">
31
+ <label for="prompt">Prompt:</label>
32
+ <textarea id="prompt" name="prompt" rows="4" required></textarea>
33
+ </div>
34
+ <div class="form-group full-width">
35
+ <label for="negative-prompt">Negative Prompt:</label>
36
+ <textarea id="negative-prompt" name="negative-prompt" rows="2"></textarea>
37
+ </div>
38
+ <div class="form-group">
39
+ <label for="aspect-ratio">Aspect Ratio:</label>
40
+ <select id="aspect-ratio" name="aspect-ratio">
41
+ <option value="16:9">16:9</option>
42
+ <option value="9:16">9:16</option>
43
+ </select>
44
+ </div>
45
+ <div class="form-group">
46
+ <label for="duration">Duration (seconds):</label>
47
+ <input type="number" id="duration" name="duration" value="8" min="5" max="8">
48
+ </div>
49
+ <div class="form-group">
50
+ <label for="resolution">Resolution:</label>
51
+ <select id="resolution" name="resolution">
52
+ <option value="720p">720p</option>
53
+ <option value="1080p">1080p</option>
54
+ </select>
55
+ </div>
56
+ <div class="form-group">
57
+ <label for="sample-count">Sample Count:</label>
58
+ <input type="number" id="sample-count" name="sample-count" value="1" min="1" max="4">
59
+ </div>
60
+ <div class="form-group">
61
+ <label for="seed">Seed:</label>
62
+ <input type="number" id="seed" name="seed" min="0" max="4294967295">
63
+ </div>
64
+ <div class="form-group">
65
+ <label for="person-generation">Person Generation:</label>
66
+ <select id="person-generation" name="person-generation">
67
+ <option value="allow_all">Allow All</option>
68
+ <option value="allow_adult">Allow Adults</option>
69
+ <option value="dont_allow">Don't Allow</option>
70
+ </select>
71
+ </div>
72
+ <div class="form-group checkbox-group">
73
+ <label for="generate-audio">Generate Audio:</label>
74
+ <input type="checkbox" id="generate-audio" name="generate-audio" checked>
75
+ </div>
76
+ <div class="form-group checkbox-group">
77
+ <label for="enhance-prompt">Enhance Prompt:</label>
78
+ <input type="checkbox" id="enhance-prompt" name="enhance-prompt" checked>
79
+ </div>
80
+ <div class="form-group full-width">
81
+ <label for="image-prompt">Image Prompt:</label>
82
+ <input type="file" id="image-prompt" name="image-prompt" accept="image/jpeg,image/png">
83
+ </div>
84
+ <div class="form-group full-width">
85
+ <label for="video-prompt">Video Prompt:</label>
86
+ <input type="file" id="video-prompt" name="video-prompt" accept="video/mp4">
87
+ </div>
88
+ </div>
89
+ <button type="submit">Generate Video</button>
90
+ </form>
91
+
92
+ <div id="video-container">
93
+ <div id="loader" class="hidden"></div>
94
+ <video id="generated-video" controls></video>
95
+ <div id="error-container" class="hidden"></div>
96
+ </div>
97
+ </main>
98
+ <script src="/app.js"></script>
99
+ </body>
100
+ </html>
frontend/style.css ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
2
+
3
+ body {
4
+ font-family: 'Roboto', sans-serif;
5
+ margin: 0;
6
+ background-color: #f0f2f5;
7
+ color: #333;
8
+ line-height: 1.6;
9
+ }
10
+
11
+ header {
12
+ background: linear-gradient(90deg, #4a90e2, #50e3c2);
13
+ color: #fff;
14
+ padding: 1.5rem;
15
+ text-align: center;
16
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
17
+ }
18
+
19
+ h1 {
20
+ margin: 0;
21
+ font-weight: 700;
22
+ }
23
+
24
+ main {
25
+ max-width: 900px;
26
+ margin: 2rem auto;
27
+ padding: 2rem;
28
+ background-color: #fff;
29
+ border-radius: 8px;
30
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
31
+ }
32
+
33
+ .form-grid {
34
+ display: grid;
35
+ grid-template-columns: repeat(2, 1fr);
36
+ gap: 1.5rem;
37
+ }
38
+
39
+ .form-group {
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+
44
+ .full-width {
45
+ grid-column: 1 / -1;
46
+ }
47
+
48
+ label {
49
+ font-weight: bold;
50
+ margin-bottom: 0.5rem;
51
+ color: #555;
52
+ }
53
+
54
+ input[type="text"],
55
+ input[type="number"],
56
+ textarea,
57
+ select {
58
+ padding: 0.75rem;
59
+ border: 1px solid #ccc;
60
+ border-radius: 4px;
61
+ font-size: 1rem;
62
+ transition: border-color 0.3s;
63
+ }
64
+
65
+ input[type="text"]:focus,
66
+ input[type="number"]:focus,
67
+ textarea:focus,
68
+ select:focus {
69
+ border-color: #4a90e2;
70
+ outline: none;
71
+ }
72
+
73
+ textarea {
74
+ resize: vertical;
75
+ }
76
+
77
+ .checkbox-group {
78
+ flex-direction: row;
79
+ align-items: center;
80
+ }
81
+
82
+ .checkbox-group label {
83
+ margin-bottom: 0;
84
+ margin-right: 0.5rem;
85
+ }
86
+
87
+ button {
88
+ grid-column: 1 / -1;
89
+ padding: 1rem;
90
+ background: linear-gradient(90deg, #50e3c2, #4a90e2);
91
+ color: #fff;
92
+ border: none;
93
+ border-radius: 4px;
94
+ cursor: pointer;
95
+ font-size: 1.1rem;
96
+ font-weight: bold;
97
+ transition: opacity 0.3s;
98
+ margin-top: 1rem;
99
+ }
100
+
101
+ button:hover {
102
+ opacity: 0.9;
103
+ }
104
+
105
+ #video-container {
106
+ margin-top: 2rem;
107
+ text-align: center;
108
+ }
109
+
110
+ #generated-video {
111
+ max-width: 100%;
112
+ display: none;
113
+ border-radius: 8px;
114
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
115
+ }
116
+
117
+ .hidden {
118
+ display: none;
119
+ }
120
+
121
+ #loader {
122
+ border: 8px solid #f3f3f3;
123
+ border-top: 8px solid #4a90e2;
124
+ border-radius: 50%;
125
+ width: 60px;
126
+ height: 60px;
127
+ animation: spin 1s linear infinite;
128
+ margin: 2rem auto;
129
+ }
130
+
131
+ @keyframes spin {
132
+ 0% { transform: rotate(0deg); }
133
+ 100% { transform: rotate(360deg); }
134
+ }
135
+
136
+ #error-container {
137
+ color: #e74c3c;
138
+ background-color: #fbeae5;
139
+ border: 1px solid #e74c3c;
140
+ padding: 1rem;
141
+ border-radius: 4px;
142
+ margin-top: 1rem;
143
+ }