ntdservices commited on
Commit
de447d7
·
verified ·
1 Parent(s): 0ef4d10

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile.txt +24 -0
  2. app.py +77 -0
  3. requirements.txt +3 -0
  4. templates/index.html +224 -0
Dockerfile.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lightweight base image
2
+ FROM python:3.10-slim
3
+
4
+ # System setup
5
+ ENV PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1 \
7
+ PIP_NO_CACHE_DIR=1
8
+
9
+ # Create app dir
10
+ WORKDIR /app
11
+
12
+ # Install Python deps early for better layer caching
13
+ COPY requirements.txt /app/
14
+ RUN pip install -r requirements.txt
15
+
16
+ # Copy app source
17
+ COPY . /app
18
+
19
+ # Expose Flask port (matches app.py: 7860)
20
+ EXPOSE 7860
21
+
22
+ # Run with Gunicorn in production
23
+ # -w: workers (tweak for your CPU); -k sync is fine here
24
+ CMD ["gunicorn", "-w", "2", "-k", "gthread", "-t", "120", "-b", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import shutil
4
+ from flask import Flask, request, jsonify, render_template
5
+ from gradio_client import Client
6
+
7
+ app = Flask(__name__)
8
+ app.secret_key = os.urandom(24)
9
+
10
+ # Where we’ll drop generated mp3s for the browser to fetch
11
+ os.makedirs("static", exist_ok=True)
12
+
13
+ # Point to your HF Space (leave as-is if this is already working for you)
14
+ TTS_SPACE_URL = "https://altafo-free-tts-unlimted-words.hf.space/"
15
+ client = Client(TTS_SPACE_URL)
16
+
17
+ @app.route("/")
18
+ def index():
19
+ # expects templates/index.html
20
+ return render_template("index.html")
21
+
22
+ @app.route("/api/ping")
23
+ def ping():
24
+ return "pong", 200
25
+
26
+ @app.route("/tts", methods=["POST"])
27
+ def tts():
28
+ """
29
+ JSON body:
30
+ { "text": "...", "voice": "en-US-AriaNeural - en-US (Female)", "rate": 0, "pitch": 0 }
31
+ Returns:
32
+ { "url": "/static/<uuid>.mp3" }
33
+ """
34
+ data = request.get_json(force=True) or {}
35
+ text_input = (data.get("text") or "").strip()
36
+ if not text_input:
37
+ return jsonify({"error": "No text provided"}), 400
38
+
39
+ voice = (data.get("voice") or "en-US-AriaNeural - en-US (Female)").strip()
40
+ try:
41
+ rate = int(data.get("rate", 0))
42
+ except Exception:
43
+ rate = 0
44
+ try:
45
+ pitch = int(data.get("pitch", 0))
46
+ except Exception:
47
+ pitch = 0
48
+
49
+ # Clamp to safe ranges
50
+ rate = max(-50, min(50, rate))
51
+ pitch = max(-20, min(20, pitch))
52
+
53
+ try:
54
+ # Call the Space
55
+ result = client.predict(
56
+ text_input,
57
+ voice,
58
+ rate,
59
+ pitch,
60
+ api_name="/tts_interface"
61
+ )
62
+ temp_path = result[0]
63
+ if not temp_path or not os.path.exists(temp_path):
64
+ return jsonify({"error": "TTS failed"}), 500
65
+
66
+ out_name = f"{uuid.uuid4().hex}.mp3"
67
+ out_path = os.path.join("static", out_name)
68
+ shutil.copy(temp_path, out_path)
69
+
70
+ return jsonify({"url": f"/static/{out_name}"})
71
+ except Exception as e:
72
+ return jsonify({"error": f"TTS request failed: {e}"}), 500
73
+
74
+
75
+ if __name__ == "__main__":
76
+ print("🔊 TTS Flask server running on http://0.0.0.0:7860")
77
+ app.run(host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ flask==3.0.3
2
+ gunicorn==21.2.0
3
+ gradio_client==1.3.0
templates/index.html ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta charset="UTF-8" />
6
+ <title>Anchor Dashboard — Text → Audio</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0b1220;--bg-soft:#10192e;--card:#121b31;--card-2:#0f1730;
10
+ --text:#e6edf7;--muted:#9fb0cf;--accent:#3a8dde;--accent-2:#6ea8ff;
11
+ --ring:rgba(58,141,222,.35);--radius:16px;--shadow:0 10px 30px rgba(2,8,23,.35);
12
+ }
13
+ *{box-sizing:border-box}
14
+ html,body{height:100%}
15
+ body{
16
+ margin:0;font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Apple Color Emoji","Segoe UI Emoji";
17
+ color:var(--text);
18
+ background: radial-gradient(1200px 700px at 10% -10%, #1a2d55 0%, transparent 60%),
19
+ radial-gradient(1200px 800px at 110% 10%, #1b164e 0%, transparent 50%),
20
+ var(--bg);
21
+ padding:0 0 48px;
22
+ }
23
+ .app-header{position:sticky;top:0;z-index:5;backdrop-filter:blur(10px);
24
+ background:linear-gradient(180deg, rgba(16,25,46,.9), rgba(16,25,46,.6));
25
+ border-bottom:1px solid rgba(255,255,255,.06);padding:16px 24px;}
26
+ .app-header h1{margin:0;font-size:18px;font-weight:600;letter-spacing:.3px;color:#dfe9ff}
27
+ .container{max-width:1000px;margin:24px auto 0;padding:0 20px;display:grid;gap:16px}
28
+ .card{background:linear-gradient(180deg,var(--card),var(--card-2));
29
+ border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);box-shadow:var(--shadow)}
30
+ .panel{padding:16px}
31
+ .section-title{padding:14px 16px;margin:0;border-bottom:1px solid rgba(255,255,255,.06);
32
+ color:#dce7ff;font-weight:600;letter-spacing:.2px}
33
+ button,.btn{
34
+ appearance:none;border:1px solid rgba(255,255,255,.08);
35
+ background:linear-gradient(180deg,#1b2645,#16213d);color:var(--text);
36
+ padding:10px 16px;border-radius:12px;cursor:pointer;
37
+ transition:transform .08s ease, box-shadow .2s ease, border-color .2s ease;
38
+ box-shadow:0 4px 12px rgba(0,0,0,.25);font-weight:600
39
+ }
40
+ button:hover{transform:translateY(-1px);border-color:var(--accent);box-shadow:0 8px 20px rgba(58,141,222,.25)}
41
+ label{color:var(--muted);font-size:14px}
42
+ select,input[type="file"],textarea{
43
+ background:#0f1730;color:var(--text);border:1px solid rgba(255,255,255,.08);
44
+ border-radius:12px;padding:10px 12px
45
+ }
46
+ select:focus,input[type="file"]:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 4px var(--ring)}
47
+ textarea{width:100%;min-height:220px;resize:vertical}
48
+ .row{display:flex;flex-wrap:wrap;gap:12px;align-items:center}
49
+ .dropzone{border:2px dashed rgba(255,255,255,.15);border-radius:14px;padding:18px;text-align:center;color:var(--muted);
50
+ background:linear-gradient(180deg,#101a35,#0e1530);cursor:pointer}
51
+ .dropzone.is-dragover{background:linear-gradient(180deg,#0f2a55,#142a5a);border-color:var(--accent);color:#e2ecff}
52
+ .helper{font-size:.9em;color:#9fb0cf;margin:8px 0 0}
53
+ #apiStatus{color:#9fb0cf;margin:10px 0}
54
+ #downloadLink{display:none;margin-left:12px;text-decoration:none;color:white}
55
+ #downloadLink:hover{text-decoration:underline}
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <header class="app-header">
60
+ <h1>Anchor Dashboard — Text to Speech</h1>
61
+ </header>
62
+
63
+ <div class="container">
64
+ <section class="card panel">
65
+ <h3 class="section-title">Your Text</h3>
66
+
67
+ <div class="row" style="justify-content:space-between">
68
+ <div style="flex:1 1 100%">
69
+ <textarea id="textInput" placeholder="Paste or type text here…"></textarea>
70
+ <p class="helper">Tip: you can also drop a <b>.txt</b> file below and we’ll load it here.</p>
71
+ </div>
72
+ </div>
73
+
74
+ <div id="dropZone" class="dropzone" style="margin-top:12px">
75
+ Drop a .txt file here or click to choose
76
+ <div style="margin-top:6px;font-size:.95em;color:#e2ecff;display:none" id="dzFile"></div>
77
+ </div>
78
+ <input id="txtFile" type="file" accept=".txt,text/plain" style="display:none" />
79
+
80
+ <h3 class="section-title" style="margin-top:16px">AI Playback</h3>
81
+ <div class="row" style="margin-top:8px">
82
+ <label>
83
+ Voice:
84
+ <select id="voiceSelect">
85
+ <option>en-US-AvaNeural - en-US (Female)</option>
86
+ <option>en-US-AndrewNeural - en-US (Male)</option>
87
+ <option>en-US-EmmaNeural - en-US (Female)</option>
88
+ <option>en-US-BrianNeural - en-US (Male)</option>
89
+ <option>en-US-EmmaMultilingualNeural - en-US (Female)</option>
90
+ <option>en-US-EricNeural - en-US (Male)</option>
91
+ <option>en-US-GuyNeural - en-US (Male)</option>
92
+ <option>en-US-JennyNeural - en-US (Female)</option>
93
+ <option>en-US-MichelleNeural - en-US (Female)</option>
94
+ <option>en-US-RogerNeural - en-US (Male)</option>
95
+ <option>en-US-SteffanNeural - en-US (Male)</option>
96
+ <option>en-US-AnaNeural - en-US (Female)</option>
97
+ <option>en-US-AndrewMultilingualNeural - en-US (Male)</option>
98
+ <option>en-US-AriaNeural - en-US (Female)</option>
99
+ <option>en-US-AvaMultilingualNeural - en-US (Female)</option>
100
+ <option>en-US-BrianMultilingualNeural - en-US (Male)</option>
101
+ <option>en-US-ChristopherNeural - en-US (Male)</option>
102
+ </select>
103
+ </label>
104
+
105
+ <label>
106
+ Speech Rate (%):
107
+ <select id="rateSelect"></select>
108
+ </label>
109
+
110
+ <label>
111
+ Pitch (Hz):
112
+ <select id="pitchSelect"></select>
113
+ </label>
114
+
115
+ <button id="generateBtn">Generate Audio</button>
116
+ <a id="downloadLink" href="#" download="tts.mp3">Download audio</a>
117
+ </div>
118
+
119
+ <div id="apiStatus"></div>
120
+ <div id="audioPlayerContainer"></div>
121
+ </section>
122
+ </div>
123
+
124
+ <script>
125
+ /* --------- rate & pitch options ---------- */
126
+ (function initRatePitch(){
127
+ const rateSel = document.getElementById("rateSelect");
128
+ const pitchSel = document.getElementById("pitchSelect");
129
+ for (let v = -50; v <= 50; v += 1){
130
+ const opt = document.createElement("option");
131
+ opt.value = String(v);
132
+ opt.textContent = v > 0 ? `+${v}` : String(v);
133
+ if (v === 0) opt.selected = true;
134
+ rateSel.appendChild(opt);
135
+ }
136
+ for (let v = -20; v <= 20; v += 1){
137
+ const opt = document.createElement("option");
138
+ opt.value = String(v);
139
+ opt.textContent = v > 0 ? `+${v}` : String(v);
140
+ if (v === 0) opt.selected = true;
141
+ pitchSel.appendChild(opt);
142
+ }
143
+ })();
144
+
145
+ /* --------- TTS trigger ---------- */
146
+ async function runTTS(){
147
+ const textEl = document.getElementById("textInput");
148
+ const status = document.getElementById("apiStatus");
149
+ const player = document.getElementById("audioPlayerContainer");
150
+ const dl = document.getElementById("downloadLink");
151
+
152
+ const text = (textEl.value || "").trim();
153
+ const voice = document.getElementById("voiceSelect").value;
154
+ const rate = parseInt(document.getElementById("rateSelect").value || "0", 10);
155
+ const pitch = parseInt(document.getElementById("pitchSelect").value || "0", 10);
156
+
157
+ if (!text){
158
+ alert("Please enter or load some text first.");
159
+ return;
160
+ }
161
+
162
+ status.textContent = "Generating audio…";
163
+ player.innerHTML = "";
164
+ dl.style.display = "none";
165
+
166
+ try{
167
+ const res = await fetch("/tts", {
168
+ method:"POST",
169
+ headers: {"Content-Type":"application/json"},
170
+ body: JSON.stringify({ text, voice, rate, pitch })
171
+ });
172
+ const data = await res.json();
173
+ if (!res.ok){
174
+ throw new Error(data?.error || "TTS failed");
175
+ }
176
+ const url = data.url;
177
+ player.innerHTML = `<audio controls style="width:100%"><source src="${url}" type="audio/mpeg"></audio>`;
178
+ dl.href = url;
179
+ dl.style.display = "inline-block";
180
+ status.textContent = "Done.";
181
+ }catch(err){
182
+ console.error(err);
183
+ status.textContent = "";
184
+ alert("TTS failed: " + err.message);
185
+ }
186
+ }
187
+ document.getElementById("generateBtn").addEventListener("click", runTTS);
188
+
189
+ /* --------- Dropzone for .txt ---------- */
190
+ const dz = document.getElementById("dropZone");
191
+ const dzFile = document.getElementById("dzFile");
192
+ const fileInput = document.getElementById("txtFile");
193
+
194
+ ["dragenter","dragover","dragleave","drop"].forEach(evt =>
195
+ document.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); })
196
+ );
197
+
198
+ ["dragenter","dragover"].forEach(evt =>
199
+ dz.addEventListener(evt, () => dz.classList.add("is-dragover"))
200
+ );
201
+ ["dragleave","drop"].forEach(evt =>
202
+ dz.addEventListener(evt, () => dz.classList.remove("is-dragover"))
203
+ );
204
+
205
+ dz.addEventListener("click", () => fileInput.click());
206
+
207
+ function loadTxtFile(file){
208
+ if (!file) return;
209
+ const ok = file.type === "text/plain" || file.name.toLowerCase().endsWith(".txt");
210
+ if (!ok){ alert("Please choose a .txt file."); return; }
211
+ const reader = new FileReader();
212
+ reader.onload = () => {
213
+ document.getElementById("textInput").value = reader.result || "";
214
+ dzFile.style.display = "block";
215
+ dzFile.textContent = file.name + " — loaded.";
216
+ };
217
+ reader.readAsText(file);
218
+ }
219
+
220
+ dz.addEventListener("drop", e => loadTxtFile(e.dataTransfer?.files?.[0]));
221
+ fileInput.addEventListener("change", e => loadTxtFile(e.target.files?.[0]));
222
+ </script>
223
+ </body>
224
+ </html>