Spaces:
Running
Running
IZERE HIRWA Roger
commited on
Commit
·
a1320d8
1
Parent(s):
1e57954
- Dockerfile +37 -0
- app.py +54 -0
- space.yaml +1 -0
- static/css/style.css +93 -0
- static/js/scripts.js +38 -0
- templates/index.html +42 -0
Dockerfile
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9-slim
|
2 |
+
|
3 |
+
# Install system dependencies
|
4 |
+
RUN apt-get update && \
|
5 |
+
apt-get install -y --no-install-recommends \
|
6 |
+
ffmpeg \
|
7 |
+
git \
|
8 |
+
wget \
|
9 |
+
&& rm -rf /var/lib/apt/lists/*
|
10 |
+
|
11 |
+
# Create checkpoints directory
|
12 |
+
RUN mkdir -p /app/SadTalker/checkpoints
|
13 |
+
|
14 |
+
# Download model weights (official SadTalker releases)
|
15 |
+
RUN wget -P /app/SadTalker/checkpoints \
|
16 |
+
https://github.com/OpenTalker/SadTalker/releases/download/v0.0.2/auido2exp_00300-model.pth \
|
17 |
+
https://github.com/OpenTalker/SadTalker/releases/download/v0.0.2/auido2pose_00140-model.pth \
|
18 |
+
https://github.com/OpenTalker/SadTalker/releases/download/v0.0.2/epoch_20.pth \
|
19 |
+
https://github.com/OpenTalker/SadTalker/releases/download/v0.0.2/facevid2vid_00189-model.pth.tar
|
20 |
+
|
21 |
+
# Clone SadTalker
|
22 |
+
RUN git clone https://github.com/OpenTalker/SadTalker.git /app/SadTalker
|
23 |
+
|
24 |
+
WORKDIR /app
|
25 |
+
|
26 |
+
# Install Python dependencies
|
27 |
+
COPY ./app/requirements.txt .
|
28 |
+
RUN pip install --no-cache-dir -r requirements.txt && \
|
29 |
+
pip install /app/SadTalker
|
30 |
+
|
31 |
+
# Copy app files
|
32 |
+
COPY ./app /app
|
33 |
+
|
34 |
+
# Force CPU mode
|
35 |
+
ENV SADTALKER_FORCE_CPU=1
|
36 |
+
EXPOSE 7860
|
37 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from flask import Flask, render_template, request, jsonify
|
3 |
+
from SadTalker import SadTalker
|
4 |
+
|
5 |
+
app = Flask(__name__)
|
6 |
+
|
7 |
+
# Initialize SadTalker with CPU
|
8 |
+
sadtalker = SadTalker(
|
9 |
+
checkpoint_path="/app/SadTalker/checkpoints",
|
10 |
+
config_path="/app/SadTalker/src/config",
|
11 |
+
device="cpu"
|
12 |
+
)
|
13 |
+
|
14 |
+
@app.route('/')
|
15 |
+
def home():
|
16 |
+
return render_template('index.html')
|
17 |
+
|
18 |
+
@app.route('/generate', methods=['POST'])
|
19 |
+
def generate():
|
20 |
+
if 'image' not in request.files:
|
21 |
+
return jsonify({"error": "No image uploaded"}), 400
|
22 |
+
|
23 |
+
image = request.files['image']
|
24 |
+
text = request.form.get('text', '')
|
25 |
+
|
26 |
+
# Save files
|
27 |
+
img_path = os.path.join('static/uploads', image.filename)
|
28 |
+
audio_path = os.path.join('static/uploads', 'audio.wav')
|
29 |
+
output_path = os.path.join('static/uploads', 'output.mp4')
|
30 |
+
|
31 |
+
image.save(img_path)
|
32 |
+
|
33 |
+
# Text-to-Speech (using gTTS)
|
34 |
+
from gtts import gTTS
|
35 |
+
tts = gTTS(text=text, lang='en')
|
36 |
+
tts.save(audio_path)
|
37 |
+
|
38 |
+
# Generate video (CPU optimized)
|
39 |
+
sadtalker.generate(
|
40 |
+
source_image=img_path,
|
41 |
+
driven_audio=audio_path,
|
42 |
+
result_dir='static/uploads',
|
43 |
+
still=True,
|
44 |
+
preprocess='crop',
|
45 |
+
enhancer='none' # Disable for CPU
|
46 |
+
)
|
47 |
+
|
48 |
+
return jsonify({
|
49 |
+
"video": f"/static/uploads/{os.path.basename(output_path)}"
|
50 |
+
})
|
51 |
+
|
52 |
+
if __name__ == '__main__':
|
53 |
+
os.makedirs('static/uploads', exist_ok=True)
|
54 |
+
app.run(host='0.0.0.0', port=7860)
|
space.yaml
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
sdk: "docker"
|
static/css/style.css
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Custom Bootstrap overrides */
|
2 |
+
:root {
|
3 |
+
--primary-color: #4e73df;
|
4 |
+
--secondary-color: #858796;
|
5 |
+
--success-color: #1cc88a;
|
6 |
+
}
|
7 |
+
|
8 |
+
body {
|
9 |
+
background-color: #f8f9fc;
|
10 |
+
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
11 |
+
}
|
12 |
+
|
13 |
+
.card {
|
14 |
+
border: none;
|
15 |
+
border-radius: 0.35rem;
|
16 |
+
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
|
17 |
+
}
|
18 |
+
|
19 |
+
.card-header {
|
20 |
+
background-color: var(--primary-color);
|
21 |
+
border-bottom: none;
|
22 |
+
}
|
23 |
+
|
24 |
+
.btn-primary {
|
25 |
+
background-color: var(--primary-color);
|
26 |
+
border-color: var(--primary-color);
|
27 |
+
}
|
28 |
+
|
29 |
+
.btn-primary:hover {
|
30 |
+
background-color: #2e59d9;
|
31 |
+
border-color: #2653d4;
|
32 |
+
}
|
33 |
+
|
34 |
+
/* Upload area styling */
|
35 |
+
.upload-area {
|
36 |
+
border: 2px dashed #d1d3e2;
|
37 |
+
border-radius: 0.35rem;
|
38 |
+
padding: 2rem;
|
39 |
+
text-align: center;
|
40 |
+
margin-bottom: 1.5rem;
|
41 |
+
cursor: pointer;
|
42 |
+
transition: all 0.3s;
|
43 |
+
}
|
44 |
+
|
45 |
+
.upload-area:hover {
|
46 |
+
border-color: var(--primary-color);
|
47 |
+
background-color: rgba(78, 115, 223, 0.05);
|
48 |
+
}
|
49 |
+
|
50 |
+
.upload-area i {
|
51 |
+
font-size: 3rem;
|
52 |
+
color: var(--secondary-color);
|
53 |
+
margin-bottom: 1rem;
|
54 |
+
}
|
55 |
+
|
56 |
+
/* Video result styling */
|
57 |
+
#result {
|
58 |
+
transition: all 0.3s;
|
59 |
+
}
|
60 |
+
|
61 |
+
#outputVideo {
|
62 |
+
border-radius: 0.35rem;
|
63 |
+
background-color: #000;
|
64 |
+
}
|
65 |
+
|
66 |
+
/* Loading spinner */
|
67 |
+
.spinner-border {
|
68 |
+
width: 1.2rem;
|
69 |
+
height: 1.2rem;
|
70 |
+
border-width: 0.15em;
|
71 |
+
}
|
72 |
+
|
73 |
+
/* Responsive adjustments */
|
74 |
+
@media (max-width: 768px) {
|
75 |
+
.card-body {
|
76 |
+
padding: 1.25rem;
|
77 |
+
}
|
78 |
+
|
79 |
+
.upload-area {
|
80 |
+
padding: 1.5rem;
|
81 |
+
}
|
82 |
+
}
|
83 |
+
|
84 |
+
/* Animation for processing state */
|
85 |
+
@keyframes pulse {
|
86 |
+
0% { opacity: 0.6; }
|
87 |
+
50% { opacity: 1; }
|
88 |
+
100% { opacity: 0.6; }
|
89 |
+
}
|
90 |
+
|
91 |
+
.processing {
|
92 |
+
animation: pulse 1.5s infinite;
|
93 |
+
}
|
static/js/scripts.js
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.getElementById('avatarForm').addEventListener('submit', async (e) => {
|
2 |
+
e.preventDefault();
|
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 |
+
// Show loading state
|
9 |
+
const btn = document.querySelector('button[type="submit"]');
|
10 |
+
btn.disabled = true;
|
11 |
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Processing...';
|
12 |
+
|
13 |
+
try {
|
14 |
+
const response = await fetch('/generate', {
|
15 |
+
method: 'POST',
|
16 |
+
body: formData
|
17 |
+
});
|
18 |
+
const data = await response.json();
|
19 |
+
|
20 |
+
// Display result
|
21 |
+
const video = document.getElementById('outputVideo');
|
22 |
+
video.src = data.video;
|
23 |
+
document.getElementById('result').classList.remove('d-none');
|
24 |
+
|
25 |
+
// Set up download
|
26 |
+
document.getElementById('downloadBtn').onclick = () => {
|
27 |
+
const a = document.createElement('a');
|
28 |
+
a.href = data.video;
|
29 |
+
a.download = 'talking_avatar.mp4';
|
30 |
+
a.click();
|
31 |
+
};
|
32 |
+
} catch (error) {
|
33 |
+
alert('Error: ' + error.message);
|
34 |
+
} finally {
|
35 |
+
btn.disabled = false;
|
36 |
+
btn.innerHTML = 'Generate Video';
|
37 |
+
}
|
38 |
+
});
|
templates/index.html
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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">
|
6 |
+
<title>Talking Avatar</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
8 |
+
<link href="../static/css/styles.css" rel="stylesheet">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="container mt-5">
|
12 |
+
<div class="row">
|
13 |
+
<div class="col-md-6 mx-auto">
|
14 |
+
<div class="card shadow">
|
15 |
+
<div class="card-header bg-primary text-white">
|
16 |
+
<h3 class="text-center">Make Your Image Talk</h3>
|
17 |
+
</div>
|
18 |
+
<div class="card-body">
|
19 |
+
<form id="avatarForm">
|
20 |
+
<div class="mb-3">
|
21 |
+
<label class="form-label">Upload Image</label>
|
22 |
+
<input type="file" class="form-control" id="imageInput" accept="image/*">
|
23 |
+
</div>
|
24 |
+
<div class="mb-3">
|
25 |
+
<label class="form-label">Text to Speak</label>
|
26 |
+
<textarea class="form-control" id="textInput" rows="3"></textarea>
|
27 |
+
</div>
|
28 |
+
<button type="submit" class="btn btn-primary w-100">Generate Video</button>
|
29 |
+
</form>
|
30 |
+
<div id="result" class="mt-4 text-center d-none">
|
31 |
+
<video id="outputVideo" controls class="w-100"></video>
|
32 |
+
<a id="downloadBtn" class="btn btn-success mt-2">Download Video</a>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
</div>
|
39 |
+
<script src="../static/js/script.js"></script>
|
40 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
41 |
+
</body>
|
42 |
+
</html>
|