IZERE HIRWA Roger commited on
Commit
a1320d8
·
1 Parent(s): 1e57954
Files changed (6) hide show
  1. Dockerfile +37 -0
  2. app.py +54 -0
  3. space.yaml +1 -0
  4. static/css/style.css +93 -0
  5. static/js/scripts.js +38 -0
  6. 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>