Spaces:
Sleeping
Sleeping
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>
|