Spaces:
Sleeping
Sleeping
IZERE HIRWA Roger
commited on
Commit
·
242a970
1
Parent(s):
134c911
- Dockerfile +7 -1
- app.py +51 -25
- static/js/scripts.js +72 -33
- templates/index.html +56 -24
Dockerfile
CHANGED
@@ -30,10 +30,16 @@ RUN wget -P /app/SadTalker/checkpoints \
|
|
30 |
# Copy application files
|
31 |
COPY . .
|
32 |
|
33 |
-
# Create necessary directories
|
34 |
RUN mkdir -p /app/static/uploads && chmod -R 777 /app/static/uploads
|
|
|
|
|
35 |
RUN mkdir -p /app/templates && chmod -R 777 /app/templates
|
36 |
|
|
|
|
|
|
|
|
|
37 |
# Add SadTalker to Python path
|
38 |
ENV PYTHONPATH="${PYTHONPATH}:/app/SadTalker/src"
|
39 |
|
|
|
30 |
# Copy application files
|
31 |
COPY . .
|
32 |
|
33 |
+
# Create necessary directories with proper structure
|
34 |
RUN mkdir -p /app/static/uploads && chmod -R 777 /app/static/uploads
|
35 |
+
RUN mkdir -p /app/static/css && chmod -R 777 /app/static/css
|
36 |
+
RUN mkdir -p /app/static/js && chmod -R 777 /app/static/js
|
37 |
RUN mkdir -p /app/templates && chmod -R 777 /app/templates
|
38 |
|
39 |
+
# Ensure static files are copied correctly
|
40 |
+
COPY static/ /app/static/
|
41 |
+
COPY templates/ /app/templates/
|
42 |
+
|
43 |
# Add SadTalker to Python path
|
44 |
ENV PYTHONPATH="${PYTHONPATH}:/app/SadTalker/src"
|
45 |
|
app.py
CHANGED
@@ -2,9 +2,9 @@ import os
|
|
2 |
import sys
|
3 |
sys.path.append('/app/SadTalker/src')
|
4 |
|
5 |
-
from flask import Flask, render_template, request, jsonify
|
6 |
|
7 |
-
app = Flask(__name__)
|
8 |
|
9 |
# Initialize SadTalker with proper import
|
10 |
try:
|
@@ -30,31 +30,57 @@ def generate():
|
|
30 |
image = request.files['image']
|
31 |
text = request.form.get('text', '')
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
audio_path = os.path.join('static/uploads', 'audio.wav')
|
36 |
-
output_path = os.path.join('static/uploads', 'output.mp4')
|
37 |
|
38 |
-
image.
|
|
|
39 |
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
if __name__ == '__main__':
|
60 |
os.makedirs('static/uploads', exist_ok=True)
|
|
|
2 |
import sys
|
3 |
sys.path.append('/app/SadTalker/src')
|
4 |
|
5 |
+
from flask import Flask, render_template, request, jsonify, send_from_directory
|
6 |
|
7 |
+
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
8 |
|
9 |
# Initialize SadTalker with proper import
|
10 |
try:
|
|
|
30 |
image = request.files['image']
|
31 |
text = request.form.get('text', '')
|
32 |
|
33 |
+
if not text.strip():
|
34 |
+
return jsonify({"error": "No text provided"}), 400
|
|
|
|
|
35 |
|
36 |
+
if not image.filename:
|
37 |
+
return jsonify({"error": "No image selected"}), 400
|
38 |
|
39 |
+
try:
|
40 |
+
# Save files
|
41 |
+
img_path = os.path.join('static/uploads', image.filename)
|
42 |
+
audio_path = os.path.join('static/uploads', 'audio.wav')
|
43 |
+
output_path = os.path.join('static/uploads', 'output.mp4')
|
44 |
+
|
45 |
+
image.save(img_path)
|
46 |
+
|
47 |
+
# Text-to-Speech (using gTTS)
|
48 |
+
from gtts import gTTS
|
49 |
+
tts = gTTS(text=text, lang='en')
|
50 |
+
tts.save(audio_path)
|
51 |
+
|
52 |
+
# Generate video (CPU optimized)
|
53 |
+
if sadtalker:
|
54 |
+
sadtalker.generate(
|
55 |
+
source_image=img_path,
|
56 |
+
driven_audio=audio_path,
|
57 |
+
result_dir='static/uploads',
|
58 |
+
still=True,
|
59 |
+
preprocess='crop',
|
60 |
+
enhancer='none' # Disable for CPU
|
61 |
+
)
|
62 |
+
else:
|
63 |
+
return jsonify({"error": "SadTalker not initialized"}), 500
|
64 |
+
|
65 |
+
return jsonify({
|
66 |
+
"video": f"/static/uploads/{os.path.basename(output_path)}"
|
67 |
+
})
|
68 |
+
except Exception as e:
|
69 |
+
return jsonify({"error": str(e)}), 500
|
70 |
+
|
71 |
+
# Debug route to check static files
|
72 |
+
@app.route('/debug/static')
|
73 |
+
def debug_static():
|
74 |
+
static_files = []
|
75 |
+
for root, dirs, files in os.walk('static'):
|
76 |
+
for file in files:
|
77 |
+
static_files.append(os.path.join(root, file))
|
78 |
+
return jsonify({"static_files": static_files})
|
79 |
+
|
80 |
+
# Explicit static file route for debugging
|
81 |
+
@app.route('/static/<path:filename>')
|
82 |
+
def static_files(filename):
|
83 |
+
return send_from_directory(app.static_folder, filename)
|
84 |
|
85 |
if __name__ == '__main__':
|
86 |
os.makedirs('static/uploads', exist_ok=True)
|
static/js/scripts.js
CHANGED
@@ -1,38 +1,77 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
const formData = new FormData();
|
5 |
-
formData.append('image', document.getElementById('imageInput').files[0]);
|
6 |
-
formData.append('text', document.getElementById('textInput').value);
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
body: formData
|
17 |
-
});
|
18 |
-
const data = await response.json();
|
19 |
|
20 |
-
|
21 |
-
const
|
22 |
-
video.src = data.video;
|
23 |
-
document.getElementById('result').classList.remove('d-none');
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
});
|
|
|
1 |
+
// Debug: Check if script is loaded
|
2 |
+
console.log('Scripts.js loaded successfully');
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
document.addEventListener('DOMContentLoaded', function() {
|
5 |
+
console.log('DOM loaded, initializing form handler');
|
6 |
+
|
7 |
+
const form = document.getElementById('avatarForm');
|
8 |
+
if (!form) {
|
9 |
+
console.error('Avatar form not found!');
|
10 |
+
return;
|
11 |
+
}
|
12 |
|
13 |
+
form.addEventListener('submit', async (e) => {
|
14 |
+
e.preventDefault();
|
15 |
+
console.log('Form submitted');
|
|
|
|
|
|
|
16 |
|
17 |
+
const imageInput = document.getElementById('imageInput');
|
18 |
+
const textInput = document.getElementById('textInput');
|
|
|
|
|
19 |
|
20 |
+
if (!imageInput.files[0]) {
|
21 |
+
alert('Please select an image');
|
22 |
+
return;
|
23 |
+
}
|
24 |
+
|
25 |
+
if (!textInput.value.trim()) {
|
26 |
+
alert('Please enter some text');
|
27 |
+
return;
|
28 |
+
}
|
29 |
+
|
30 |
+
const formData = new FormData();
|
31 |
+
formData.append('image', imageInput.files[0]);
|
32 |
+
formData.append('text', textInput.value);
|
33 |
+
|
34 |
+
// Show loading state
|
35 |
+
const btn = document.querySelector('button[type="submit"]');
|
36 |
+
btn.disabled = true;
|
37 |
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span> Processing...';
|
38 |
+
|
39 |
+
try {
|
40 |
+
console.log('Sending request to /generate');
|
41 |
+
const response = await fetch('/generate', {
|
42 |
+
method: 'POST',
|
43 |
+
body: formData
|
44 |
+
});
|
45 |
+
|
46 |
+
if (!response.ok) {
|
47 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
48 |
+
}
|
49 |
+
|
50 |
+
const data = await response.json();
|
51 |
+
console.log('Response received:', data);
|
52 |
+
|
53 |
+
if (data.error) {
|
54 |
+
throw new Error(data.error);
|
55 |
+
}
|
56 |
+
|
57 |
+
// Display result
|
58 |
+
const video = document.getElementById('outputVideo');
|
59 |
+
video.src = data.video;
|
60 |
+
document.getElementById('result').classList.remove('d-none');
|
61 |
+
|
62 |
+
// Set up download
|
63 |
+
document.getElementById('downloadBtn').onclick = () => {
|
64 |
+
const a = document.createElement('a');
|
65 |
+
a.href = data.video;
|
66 |
+
a.download = 'talking_avatar.mp4';
|
67 |
+
a.click();
|
68 |
+
};
|
69 |
+
} catch (error) {
|
70 |
+
console.error('Error:', error);
|
71 |
+
alert('Error: ' + error.message);
|
72 |
+
} finally {
|
73 |
+
btn.disabled = false;
|
74 |
+
btn.innerHTML = '<i class="fas fa-magic me-2"></i>Generate Video';
|
75 |
+
}
|
76 |
+
});
|
77 |
});
|
templates/index.html
CHANGED
@@ -2,41 +2,73 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6 |
-
<title>Talking Avatar</title>
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
</head>
|
10 |
<body>
|
11 |
-
<div class="container
|
12 |
-
<div class="row">
|
13 |
-
<div class="col-
|
14 |
-
<div class="card
|
15 |
-
<div class="card-header
|
16 |
-
<
|
17 |
</div>
|
18 |
<div class="card-body">
|
19 |
-
<form id="avatarForm">
|
20 |
-
|
21 |
-
|
22 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
</div>
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
27 |
</div>
|
28 |
-
|
|
|
|
|
|
|
|
|
29 |
</form>
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
</div>
|
35 |
</div>
|
36 |
</div>
|
37 |
</div>
|
38 |
</div>
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
41 |
</body>
|
42 |
</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>AI Talking Avatar Generator</title>
|
7 |
+
|
8 |
+
<!-- Bootstrap CSS -->
|
9 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
10 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
11 |
+
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;600;700;800;900&display=swap" rel="stylesheet">
|
12 |
+
|
13 |
+
<!-- Custom CSS -->
|
14 |
+
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
15 |
</head>
|
16 |
<body>
|
17 |
+
<div class="container py-5">
|
18 |
+
<div class="row justify-content-center">
|
19 |
+
<div class="col-lg-8">
|
20 |
+
<div class="card">
|
21 |
+
<div class="card-header text-white">
|
22 |
+
<h4 class="mb-0"><i class="fas fa-video me-2"></i>AI Talking Avatar Generator</h4>
|
23 |
</div>
|
24 |
<div class="card-body">
|
25 |
+
<form id="avatarForm" enctype="multipart/form-data">
|
26 |
+
<!-- Image Upload -->
|
27 |
+
<div class="mb-4">
|
28 |
+
<label class="form-label">Upload Photo</label>
|
29 |
+
<div class="upload-area" onclick="document.getElementById('imageInput').click()">
|
30 |
+
<i class="fas fa-cloud-upload-alt"></i>
|
31 |
+
<p class="mb-0">Click to upload an image</p>
|
32 |
+
<small class="text-muted">Supports JPG, PNG formats</small>
|
33 |
+
</div>
|
34 |
+
<input type="file" id="imageInput" class="d-none" accept="image/*" required>
|
35 |
</div>
|
36 |
+
|
37 |
+
<!-- Text Input -->
|
38 |
+
<div class="mb-4">
|
39 |
+
<label for="textInput" class="form-label">Enter Text</label>
|
40 |
+
<textarea id="textInput" class="form-control" rows="4"
|
41 |
+
placeholder="Enter the text you want the avatar to speak..." required></textarea>
|
42 |
</div>
|
43 |
+
|
44 |
+
<!-- Submit Button -->
|
45 |
+
<button type="submit" class="btn btn-primary btn-lg w-100">
|
46 |
+
<i class="fas fa-magic me-2"></i>Generate Video
|
47 |
+
</button>
|
48 |
</form>
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
|
52 |
+
<!-- Result Section -->
|
53 |
+
<div id="result" class="card mt-4 d-none">
|
54 |
+
<div class="card-header">
|
55 |
+
<h5 class="mb-0"><i class="fas fa-play-circle me-2"></i>Generated Video</h5>
|
56 |
+
</div>
|
57 |
+
<div class="card-body text-center">
|
58 |
+
<video id="outputVideo" controls class="w-100 mb-3" style="max-height: 400px;"></video>
|
59 |
+
<button id="downloadBtn" class="btn btn-success">
|
60 |
+
<i class="fas fa-download me-2"></i>Download Video
|
61 |
+
</button>
|
62 |
</div>
|
63 |
</div>
|
64 |
</div>
|
65 |
</div>
|
66 |
</div>
|
67 |
+
|
68 |
+
<!-- Bootstrap JS -->
|
69 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
70 |
+
|
71 |
+
<!-- Custom JS -->
|
72 |
+
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
|
73 |
</body>
|
74 |
</html>
|