dronesplace commited on
Commit
5af8b8b
·
verified ·
1 Parent(s): 1251a18

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -248
app.py CHANGED
@@ -1,253 +1,114 @@
1
- # app.py
2
- import os
3
- import uuid
4
- import tempfile
5
- from pathlib import Path
6
- from io import BytesIO
7
- import numpy as np
8
- from PIL import Image, ImageDraw, ImageOps
9
  import gradio as gr
10
- import moviepy.editor as mpy
11
- from pydub import AudioSegment
12
- import soundfile as sf
13
- import math
14
- import random
15
-
16
- # Try faster_whisper first (optional on Spaces), fallback to whisper
17
- try:
18
- from faster_whisper import WhisperModel
19
- WHISPER_AVAILABLE = True
20
- except Exception:
21
- WHISPER_AVAILABLE = False
22
- import whisper
23
-
24
- # Transformers LLM (Flan-T5 small)
25
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline
26
-
27
- # gTTS for multilingual TTS
28
  from gtts import gTTS
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- # ---------- Model setup (load once) ----------
31
- MODEL_NAME = "google/flan-t5-small"
32
- tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
33
- model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME)
34
- llm = pipeline("text2text-generation", model=model, tokenizer=tokenizer)
35
-
36
- # Whisper
37
- if WHISPER_AVAILABLE:
38
- whisper_model = WhisperModel("small", device="cpu", compute_type="int8")
39
- else:
40
- whisper_model = whisper.load_model("small")
41
-
42
- # ---------- Helpers ----------
43
- def transcribe_audio(fp, lang=None):
44
- """Return text from audio file path fp."""
45
- try:
46
- if WHISPER_AVAILABLE:
47
- segments, info = whisper_model.transcribe(fp, language=lang) if lang else whisper_model.transcribe(fp)
48
- text = " ".join([s.text for s in segments])
49
- return text
50
- else:
51
- res = whisper_model.transcribe(fp, language=lang) if lang else whisper_model.transcribe(fp)
52
- return res["text"]
53
- except Exception as e:
54
  return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- def ask_llm(user_text, history=None):
57
- """Simple LLM wrapper: keep replies short and tutor style."""
58
- prompt = f"You are a friendly English tutor and helpful assistant. Reply concisely, give an example if appropriate, and ask one follow-up question. User said: {user_text}"
59
- out = llm(prompt, max_length=256, do_sample=False)
60
- return out[0]["generated_text"]
61
-
62
- def tts_gtts(text, out_path, lang="en"):
63
- """gTTS synthesis (multilingual)."""
64
- if not text or len(text.strip())==0:
65
- # fallback silence
66
- silent = AudioSegment.silent(duration=500)
67
- silent.export(out_path, format="mp3")
68
- return out_path
69
- tts = gTTS(text=text, lang=lang, slow=False)
70
- tts.save(out_path)
71
- return out_path
72
-
73
- def make_envelope(audio_path, fps=25):
74
- """Compute amplitude envelope per frame from audio file."""
75
- seg = AudioSegment.from_file(audio_path)
76
- samples = np.array(seg.get_array_of_samples()).astype(np.float32)
77
- if seg.channels > 1:
78
- samples = samples.reshape((-1, seg.channels)).mean(axis=1)
79
- if samples.size == 0:
80
- return np.zeros(1)
81
- samples = samples / (np.max(np.abs(samples)) + 1e-9)
82
- duration_s = seg.duration_seconds
83
- n_frames = max(1, int(duration_s * fps))
84
- parts = np.array_split(samples, n_frames)
85
- env = np.array([np.sqrt(np.mean(p**2)) if p.size>0 else 0.0 for p in parts])
86
- env = (env - env.min()) / (env.max()-env.min()+1e-9)
87
- return env
88
-
89
- def create_talking_video(image_pil, audio_path, out_video_path, fps=25, emotion="neutral"):
90
- """
91
- Create an MP4 by overlaying animated 'jaw' effect on the image, synced to audio envelope.
92
- Also adds simple blink and head-tilt micro gestures based on 'emotion'.
93
- """
94
- envelope = make_envelope(audio_path, fps=fps)
95
- duration = max(0.5, len(envelope)/fps)
96
- w,h = image_pil.size
97
-
98
- # jaw box (estimate lower center)
99
- jw = int(w * 0.26); jh = int(h * 0.08)
100
- jx = int(w*0.5 - jw/2); jy = int(h*0.68 - jh/2)
101
-
102
- # emotion-driven head tilt/scale patterns
103
- if emotion == "happy":
104
- head_tilt = lambda t: math.sin(2*math.pi*t/duration)*2.2
105
- elif emotion == "thinking":
106
- head_tilt = lambda t: math.sin(2*math.pi*t/duration)*-2.5
107
- elif emotion == "surprised":
108
- head_tilt = lambda t: math.sin(2*math.pi*t/duration)*1.8
109
- else:
110
- head_tilt = lambda t: math.sin(2*math.pi*t/duration)*0.8
111
-
112
- def make_frame(t):
113
- i = min(int(t*fps), len(envelope)-1)
114
- level = float(envelope[i])
115
- frame = image_pil.copy().convert("RGBA")
116
- draw = ImageDraw.Draw(frame, 'RGBA')
117
-
118
- # subtle head tilt via shear or rotate (simple)
119
- angle = head_tilt(t)
120
- frame = frame.rotate(angle, resample=Image.BICUBIC, center=(w//2, h//3), expand=False)
121
-
122
- # mouth ellipse overlay (simulate opening)
123
- mouth_h = int(jh * (1.0 + level*1.2))
124
- mouth_y = int(jy + jh - mouth_h/2)
125
- alpha = int(20 + level*100)
126
- draw.ellipse([jx, mouth_y, jx+jw, mouth_y+mouth_h], fill=(20,20,20, alpha))
127
-
128
- # blink: occasionally draw eyelid rectangles based on time
129
- # simple periodic blink
130
- if (int(t*2) % 7) == 0 and random.random()>0.65:
131
- # top lid
132
- draw.rectangle([0, 0, w, int(h*0.23)], fill=(245,245,255,230))
133
-
134
- return np.asarray(frame)
135
-
136
- clip = mpy.VideoClip(make_frame, duration=duration)
137
- audio = mpy.AudioFileClip(audio_path)
138
- clip = clip.set_audio(audio)
139
- clip.write_videofile(out_video_path, fps=fps, codec="libx264", audio_codec="aac", verbose=False, logger=None)
140
- return out_video_path
141
-
142
- def detect_language_hint(text):
143
- # crude hint: check for non-ascii characters to switch language; default en
144
- if any(ord(ch) > 127 for ch in text):
145
- return "auto"
146
- return "en"
147
-
148
- # Emotion detection: tiny heuristic
149
- def detect_emotion_from_text(text):
150
- t = text.lower()
151
- if any(w in t for w in ["love","like","happy","great","awesome","good"]):
152
- return "happy"
153
- if any(w in t for w in ["why","how","think","wonder","question","confused"]):
154
- return "thinking"
155
- if any(w in t for w in ["wow","surprise","omg","amazed","shocked"]):
156
- return "surprised"
157
- if any(w in t for w in ["sorry","shy","nervous","awkward"]):
158
- return "shy"
159
- return "neutral"
160
-
161
- # ---------- Gradio app function ----------
162
- def process(image, upload_audio, mic_audio, typed_text):
163
- # Save working folder
164
- uid = str(uuid.uuid4())[:8]
165
- tmp = Path(tempfile.gettempdir()) / f"space_{uid}"
166
- tmp.mkdir(parents=True, exist_ok=True)
167
-
168
- # Ensure image
169
- if image is None:
170
- return None, "Please upload an avatar image (head & shoulders).", None
171
- if isinstance(image, np.ndarray):
172
- image_pil = Image.fromarray(image)
173
- else:
174
- image_pil = Image.open(image).convert("RGBA")
175
-
176
- # Determine input audio or typed text
177
- user_text = ""
178
- lang_hint = "en"
179
- audio_in = None
180
-
181
- if typed_text and typed_text.strip():
182
- user_text = typed_text.strip()
183
- lang_hint = detect_language_hint(user_text)
184
- else:
185
- # prioritize mic_audio, then upload_audio
186
- audio_file = None
187
- if mic_audio:
188
- audio_file = mic_audio
189
- elif upload_audio:
190
- audio_file = upload_audio
191
-
192
- if audio_file:
193
- # save
194
- audio_path = tmp / "user_in.wav"
195
- with open(audio_path, "wb") as f:
196
- f.write(audio_file.read())
197
- # transcribe
198
- try:
199
- user_text = transcribe_audio(str(audio_path), lang=None)
200
- except Exception as e:
201
- user_text = ""
202
- lang_hint = detect_language_hint(user_text)
203
- audio_in = str(audio_path)
204
-
205
- if not user_text:
206
- return None, "Couldn't capture text. Try typing or recording again.", None
207
-
208
- # Ask LLM
209
- reply = ask_llm(user_text)
210
-
211
- # Emotion
212
- emotion = detect_emotion_from_text(user_text + " " + reply)
213
-
214
- # TTS: generate mp3
215
- tts_path = tmp / "reply.mp3"
216
- try:
217
- tts_gtts(reply, str(tts_path), lang=lang_hint)
218
- except Exception:
219
- # fallback to english
220
- tts_gtts(reply, str(tts_path), lang="en")
221
-
222
- # Create talking clip
223
- out_video = tmp / "talking.mp4"
224
- create_talking_video(image_pil, str(tts_path), str(out_video), fps=25, emotion=emotion)
225
-
226
- # Return video file, text, audio
227
- return str(out_video), reply, str(tts_path)
228
-
229
- # ---------- Gradio UI ----------
230
- title = "No-install Avatar Companion — Hugging Face Space (Free)"
231
- desc = "Upload a single avatar image (head+shoulders). Speak or type. The app transcribes, replies, synthesizes voice, and auto-generates a talking clip (MP4) with mouth animation and simple gestures."
232
-
233
- demo = gr.Interface(
234
- fn=process,
235
- inputs=[
236
- gr.Image(type="pil", label="Upload avatar (head & shoulders PNG)"),
237
- gr.Audio(source="upload", type="file", label="Upload audio (optional)"),
238
- gr.Audio(source="microphone", type="file", label="Record via mic (optional)"),
239
- gr.Textbox(lines=2, placeholder="Or type your message (optional)", label="Type message")
240
- ],
241
- outputs=[
242
- gr.Video(label="Talking clip (MP4)"),
243
- gr.Textbox(label="Assistant reply"),
244
- gr.Audio(label="Reply audio (mp3)", type="file")
245
- ],
246
- title=title,
247
- description=desc,
248
- allow_flagging="never",
249
- examples=[]
250
- )
251
-
252
- if __name__ == "__main__":
253
- demo.launch()
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import torch
3
+ import cv2
4
+ import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from gtts import gTTS
6
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, WhisperProcessor, WhisperForConditionalGeneration
7
+ from PIL import Image
8
+ import ffmpeg
9
+ import tempfile
10
+ import os
11
+
12
+ # -----------------------
13
+ # Load Models
14
+ # -----------------------
15
+
16
+ device = "cpu"
17
 
18
+ # Speech-to-text (Whisper small)
19
+ whisper_processor = WhisperProcessor.from_pretrained("openai/whisper-small")
20
+ whisper_model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small").to(device)
21
+
22
+ # Text generation (Flan-T5 small)
23
+ tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-small")
24
+ t5_model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-small").to(device)
25
+
26
+ # -----------------------
27
+ # Helper Functions
28
+ # -----------------------
29
+
30
+ def transcribe(audio):
31
+ if audio is None:
 
 
 
 
 
 
 
 
 
 
32
  return ""
33
+ audio = whisper_processor(audio["array"], sampling_rate=16000, return_tensors="pt")
34
+ result = whisper_model.generate(audio["input_features"])
35
+ return whisper_processor.batch_decode(result, skip_special_tokens=True)[0]
36
+
37
+ def reply(text):
38
+ inp = tokenizer(text, return_tensors="pt")
39
+ out = t5_model.generate(**inp, max_length=120)
40
+ return tokenizer.decode(out[0], skip_special_tokens=True)
41
+
42
+ def synth_voice(text, path):
43
+ tts = gTTS(text=text, lang="en", tld="com", slow=False)
44
+ tts.save(path)
45
+ return path
46
+
47
+ def animate_avatar(image, audio_path):
48
+ avatar = Image.open(image).convert("RGBA")
49
+ w, h = avatar.size
50
+ avatar_np = np.array(avatar)
51
+
52
+ # Extract audio amplitude → fake lip motion
53
+ import wave
54
+ with wave.open(audio_path, "rb") as wav:
55
+ frames = wav.readframes(-1)
56
+ audio_np = np.frombuffer(frames, dtype=np.int16)
57
+ amp = np.abs(audio_np)[::2000] # downsample amplitude curve
58
+
59
+ frames_list = []
60
+ for a in amp:
61
+ frame = avatar_np.copy()
62
+ intensity = min(8, int(a / 3000))
63
+ frame[h - 40 : h - 20, w//2 - 20 : w//2 + 20, 3] = 255 - intensity * 20
64
+ frames_list.append(frame)
65
+
66
+ # Export to video
67
+ temp_video = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
68
+ out = cv2.VideoWriter(temp_video, cv2.VideoWriter_fourcc(*"mp4v"), 20, (w, h))
69
+
70
+ for f in frames_list:
71
+ out.write(cv2.cvtColor(f, cv2.COLOR_RGBA2BGR))
72
+ out.release()
73
+
74
+ return temp_video
75
+
76
+ # -----------------------
77
+ # Main Chat Logic
78
+ # -----------------------
79
+
80
+ def chat(image, audio, text):
81
+ user_input = text if text else transcribe(audio)
82
+ if not user_input:
83
+ return "Say something!", None
84
+
85
+ ai_answer = reply(user_input)
86
+
87
+ # TTS
88
+ temp_audio = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False).name
89
+ synth_voice(ai_answer, temp_audio)
90
+
91
+ # Talking avatar
92
+ video = animate_avatar(image, temp_audio)
93
+
94
+ return ai_answer, video
95
+
96
+ # -----------------------
97
+ # Gradio UI
98
+ # -----------------------
99
+
100
+ with gr.Blocks() as interface:
101
+ gr.Markdown("## 🧚‍♀️ AI Avatar Companion — Free & No-Install")
102
+
103
+ avatar = gr.Image(type="filepath", label="Upload Avatar PNG")
104
+ audio = gr.Audio(source="microphone", type="numpy", label="Speak")
105
+ txt = gr.Textbox(label="Or type your message")
106
+
107
+ out_text = gr.Textbox(label="AI Response")
108
+ out_video = gr.Video(label="Talking Avatar")
109
+
110
+ submit = gr.Button("Talk")
111
+
112
+ submit.click(chat, inputs=[avatar, audio, txt], outputs=[out_text, out_video])
113
 
114
+ interface.launch()