File size: 14,174 Bytes
11e0218
 
 
 
 
1da8ab5
 
4f6f407
11e0218
 
309470c
 
 
 
11e0218
4f6f407
e847355
4f6f407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11e0218
4f6f407
309470c
e847355
 
 
 
 
 
 
 
 
 
 
 
 
 
4f6f407
309470c
4f6f407
 
 
 
309470c
4f6f407
e847355
4f6f407
 
309470c
4f6f407
 
 
 
309470c
4f6f407
 
 
e847355
4f6f407
 
 
 
11e0218
309470c
 
11e0218
309470c
 
 
 
 
49d15bb
 
 
 
 
 
 
 
 
 
 
309470c
 
 
 
 
 
1da8ab5
309470c
4f6f407
1da8ab5
309470c
11e0218
e847355
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df4422d
1da8ab5
 
df4422d
1da8ab5
 
df4422d
1da8ab5
df4422d
1da8ab5
 
 
 
df4422d
1da8ab5
df4422d
1da8ab5
df4422d
 
 
 
 
 
1da8ab5
 
df4422d
1da8ab5
 
df4422d
1da8ab5
df4422d
 
 
1da8ab5
 
 
e847355
1da8ab5
 
 
4f6f407
1da8ab5
e847355
1da8ab5
 
 
 
 
 
 
 
 
df4422d
e847355
11e0218
1da8ab5
11e0218
 
 
 
 
 
1da8ab5
11e0218
 
 
 
 
 
 
 
 
df4422d
11e0218
df4422d
11e0218
 
1da8ab5
11e0218
 
 
1da8ab5
11e0218
 
 
 
df4422d
11e0218
 
4f6f407
11e0218
1da8ab5
 
 
 
 
 
 
 
 
e847355
4f6f407
1da8ab5
309470c
1da8ab5
309470c
df4422d
 
e847355
11e0218
1da8ab5
 
 
 
e847355
1da8ab5
 
309470c
1da8ab5
309470c
1da8ab5
 
e847355
11e0218
1da8ab5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import gradio as gr
import edge_tts
import asyncio
import tempfile
import os
import time
import base64
from datetime import datetime
from pydub import AudioSegment

VOICE      = "en-US-AndrewMultilingualNeural"
CHUNK_SIZE = 5000
HF_USER    = "Cracked-User"
HF_REPO    = "tts-audiobooks"

# ── Exact same logic as Colab ─────────────────────────────────────────────────
def run_tts(text, custom_name=''):
    chunks     = [text[i:i+CHUNK_SIZE] for i in range(0, len(text), CHUNK_SIZE)]
    total      = len(chunks)
    parts      = []
    chunk_times = []
    batch_start = time.time()
    tmp_dir    = tempfile.mkdtemp()

    yield None, '⏳ Total chunks: ' + str(total), make_player(None), ''

    for i, chunk in enumerate(chunks):
        path = tmp_dir + '/chunk_' + str(i).zfill(5) + '.mp3'

        if chunk_times:
            avg     = sum(chunk_times) / len(chunk_times)
            eta     = int(avg * (total - i))
            elapsed = int(time.time() - batch_start)
            status  = (
                'Chunk ' + str(i+1) + '/' + str(total) +
                '  ETA: ' + str(eta//60) + 'm ' + str(eta%60) + 's' +
                '  Elapsed: ' + str(elapsed//60) + 'm ' + str(elapsed%60) + 's'
            )
        else:
            status = 'Chunk ' + str(i+1) + '/' + str(total) + '  ETA: calculating...'

        yield None, status, make_player(None), ''

        try:
            async def save(chunk=chunk, path=path):
                comm = edge_tts.Communicate(chunk, VOICE)
                await comm.save(path)
            asyncio.run(save())
            parts.append(path)
        except Exception as e:
            try:
                time.sleep(5)
                asyncio.run(save())
                parts.append(path)
            except Exception as e2:
                yield None, '❌ Failed chunk ' + str(i+1) + ': ' + str(e2), make_player(None), ''
                return

        chunk_times.append(time.time() - batch_start - sum(chunk_times))

        # Log progress to HF so you can check after refresh
        avg2    = sum(chunk_times) / len(chunk_times)
        eta2    = int(avg2 * (total - i - 1))
        elapsed2 = int(time.time() - batch_start)
        pct2    = int(((i+1) / total) * 100)
        log_msg = (
            'πŸ”Š Chunk ' + str(i+1) + ' / ' + str(total) + '  (' + str(pct2) + '%)\n' +
            '⏱️ Elapsed: ' + str(elapsed2//60) + 'm ' + str(elapsed2%60) + 's\n' +
            '⏳ ETA: ' + str(eta2//60) + 'm ' + str(eta2%60) + 's\n' +
            'πŸ• Updated: ' + datetime.now().strftime('%H:%M:%S')
        )
        import threading
        threading.Thread(target=log_progress, args=(log_msg, os.environ.get('HF_TOKEN', '')), daemon=True).start()

    yield None, 'Merging all chunks...', make_player(None), ''

    combined = AudioSegment.empty()
    for p in sorted(parts):
        if os.path.exists(p):
            combined += AudioSegment.from_file(p) + AudioSegment.silent(200)

    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    filename  = (custom_name.strip().replace(' ', '_') + '.mp3') if custom_name and custom_name.strip() else ('andrew_' + timestamp + '.mp3')
    out_path  = tempfile.mktemp(suffix='.mp3')
    combined.export(out_path, format='mp3', bitrate='192k')

    # Cleanup chunks
    for p in parts:
        try: os.remove(p)
        except: pass

    total_time = int(time.time() - batch_start)
    done_msg   = 'DONE! Time: ' + str(total_time//60) + 'm ' + str(total_time%60) + 's'

    log_progress('βœ… DONE! Uploading audio file...\nπŸ• ' + datetime.now().strftime('%H:%M:%S'), os.environ.get('HF_TOKEN', ''))
    yield None, done_msg + '\nUploading to HF...', make_player(None), ''
    hf_msg = upload_to_hf(out_path, filename)

    yield out_path, done_msg, make_player(out_path), hf_msg

# ── Upload to HF Dataset ──────────────────────────────────────────────────────
def upload_to_hf(file_path, filename):
    try:
        from huggingface_hub import HfApi
        token = os.environ.get('HF_TOKEN', '')
        if not token:
            return '⚠️ No HF_TOKEN secret found.'
        api = HfApi()
        # Create repo if it doesn't exist
        try:
            api.create_repo(
                repo_id=HF_USER + '/' + HF_REPO,
                repo_type='dataset',
                private=True,
                token=token,
                exist_ok=True
            )
        except:
            pass
        api.upload_file(
            path_or_fileobj=file_path,
            path_in_repo='audiobooks/' + filename,
            repo_id=HF_USER + '/' + HF_REPO,
            repo_type='dataset',
            token=token
        )
        url = 'https://huggingface.co/datasets/' + HF_USER + '/' + HF_REPO
        return 'βœ… Saved!\nπŸ”— ' + url
    except Exception as e:
        return '❌ Upload error: ' + str(e)

# ── Log progress to HF Dataset ───────────────────────────────────────────────
def log_progress(msg, token):
    try:
        from huggingface_hub import HfApi
        if not token:
            return
        api = HfApi()
        log_path = '/tmp/progress.txt'
        with open(log_path, 'w') as f:
            f.write(msg)
        api.upload_file(
            path_or_fileobj=log_path,
            path_in_repo='progress.txt',
            repo_id=HF_USER + '/' + HF_REPO,
            repo_type='dataset',
            token=token
        )
    except:
        pass

# ── Custom HTML player ────────────────────────────────────────────────────────
def make_player(file_path):
    if not file_path or not os.path.exists(file_path):
        return '<div style="color:#3a3a5a;padding:12px;text-align:center;border:1px dashed #2a2a3a;border-radius:10px;">🎧 Player appears here after generation</div>'
    with open(file_path, 'rb') as f:
        b64 = base64.b64encode(f.read()).decode()
    return '''
<div style="background:#0e0e1c;border:1px solid #2a2a3a;border-radius:14px;padding:20px;margin-top:10px;">
  <div style="color:#ffd700;font-family:sans-serif;font-weight:700;font-size:0.78rem;
              letter-spacing:0.06em;text-transform:uppercase;margin-bottom:12px;">
    🎧 Preview Player
  </div>
  <audio id="tts-player" controls style="width:100%;margin-bottom:14px;accent-color:#ffd700;">
    <source src="data:audio/mpeg;base64,''' + b64 + '''" type="audio/mpeg">
  </audio>
  <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
    <span style="color:#9ca3af;font-size:0.82rem;">Speed:</span>
    <button onclick="setSpd(this,0.75)" style="background:#1a1a2e;border:1px solid #3a3a5c;border-radius:8px;color:#9ca3af;padding:5px 12px;cursor:pointer;font-size:0.82rem;">0.75x</button>
    <button onclick="setSpd(this,1.0)" style="background:#b8860b;border:none;border-radius:8px;color:white;padding:5px 12px;cursor:pointer;font-size:0.82rem;">1x</button>
    <button onclick="setSpd(this,1.25)" style="background:#1a1a2e;border:1px solid #3a3a5c;border-radius:8px;color:#9ca3af;padding:5px 12px;cursor:pointer;font-size:0.82rem;">1.25x</button>
    <button onclick="setSpd(this,1.5)" style="background:#1a1a2e;border:1px solid #3a3a5c;border-radius:8px;color:#9ca3af;padding:5px 12px;cursor:pointer;font-size:0.82rem;">1.5x</button>
    <button onclick="setSpd(this,1.75)" style="background:#1a1a2e;border:1px solid #3a3a5c;border-radius:8px;color:#9ca3af;padding:5px 12px;cursor:pointer;font-size:0.82rem;">1.75x</button>
    <button onclick="setSpd(this,2.0)" style="background:#1a1a2e;border:1px solid #3a3a5c;border-radius:8px;color:#9ca3af;padding:5px 12px;cursor:pointer;font-size:0.82rem;">2x</button>
  </div>
  <script>
    function setSpd(btn, s) {
      var p = document.getElementById('tts-player');
      if(p) p.playbackRate = s;
      btn.closest('div').querySelectorAll('button').forEach(function(b){
        b.style.background='#1a1a2e'; b.style.border='1px solid #3a3a5c'; b.style.color='#9ca3af';
      });
      btn.style.background='#b8860b'; btn.style.border='none'; btn.style.color='white';
    }
  </script>
</div>'''

def generate_from_text(text, custom_name=''):
    if not text.strip():
        yield None, '❌ Please enter some text!', make_player(None), ''
        return
    yield from run_tts(text)

def generate_from_file(file, custom_name=''):
    if file is None:
        yield None, '❌ Please upload a .txt file!', make_player(None), ''
        return
    try:
        with open(file, 'r', encoding='utf-8') as f:
            text = f.read()
        if not text.strip():
            yield None, '❌ File is empty!', make_player(None), ''
            return
        yield None, 'πŸ“„ Loaded ' + str(len(text)) + ' chars. Starting...', make_player(None), ''
        yield from run_tts(text, custom_name)
    except Exception as e:
        yield None, '❌ Could not read file: ' + str(e), make_player(None), ''

# ── CSS ───────────────────────────────────────────────────────────────────────
css = """
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:wght@300;400;500&display=swap');
* { font-family: 'DM Sans', sans-serif; }
body, .gradio-container { background: #07070f !important; color: #e8e8f5 !important; }
.gradio-container { max-width: 900px !important; margin: 0 auto !important; }
h1 { font-family: 'Syne', sans-serif !important; font-weight: 800 !important;
     font-size: 2.2rem !important;
     background: linear-gradient(135deg, #ffd700, #ff8c00, #ff4500) !important;
     -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important;
     text-align: center !important; margin-bottom: 0.2rem !important; }
.subtitle { text-align:center; color:#5a5a7a; font-size:0.88rem; margin-bottom:1.5rem; }
label { font-family:'Syne',sans-serif !important; font-weight:700 !important;
        font-size:0.78rem !important; color:#ffd700 !important;
        letter-spacing:0.06em !important; text-transform:uppercase !important; }
textarea { background:#0e0e1c !important; border:1px solid #2a2a3a !important;
    border-radius:10px !important; color:#e8e8f5 !important; padding:12px !important; font-size:0.95rem !important; }
textarea:focus { border-color:#ffd700 !important; outline:none !important; }
.block { background:#0d0d1a !important; border:1px solid #1a1a2e !important;
         border-radius:14px !important; padding:1.2rem !important; }
.btn-gen { background:linear-gradient(135deg,#b8860b,#ff8c00) !important;
    border:none !important; border-radius:12px !important; color:white !important;
    font-family:'Syne',sans-serif !important; font-weight:700 !important;
    font-size:1.05rem !important; padding:16px !important; width:100% !important; }
.btn-gen:hover { opacity:0.85 !important; }
.tip { background:#0e0e1c; border-left:3px solid #ffd700; border-radius:0 8px 8px 0;
       padding:10px 14px; font-size:0.82rem; color:#9ca3af; margin:0.5rem 0; }
"""

# ── GUI ───────────────────────────────────────────────────────────────────────
with gr.Blocks(css=css, title='Andrew TTS') as demo:
    gr.HTML('<h1>πŸŽ™οΈ Andrew TTS</h1>')
    gr.HTML('<p class="subtitle">Microsoft Andrew voice Β· Exact Colab quality Β· Auto saves to HF Dataset</p>')

    with gr.Tabs():
        with gr.Tab('πŸ“  Paste Text'):
            with gr.Row():
                with gr.Column(scale=2):
                    text_input = gr.Textbox(
                        label='Your Text',
                        placeholder='Paste your novel chapter or any text here β€” no limit!',
                        lines=12
                    )
                    name1 = gr.Textbox(label='File Name (optional)', placeholder='e.g. chapter_102 β€” leave blank for auto name')
                    btn1 = gr.Button('πŸŽ™οΈ Generate Audio', elem_classes='btn-gen')
                with gr.Column(scale=1):
                    gr.HTML('<div class="tip">β€’ No character limit<br>β€’ Live progress + ETA<br>β€’ Auto saves to HF Dataset forever<br>β€’ Speed control 0.75x–2x</div>')
                    status1 = gr.Textbox(label='Progress', interactive=False, lines=3)
                    hf1     = gr.Textbox(label='Storage Status', interactive=False, lines=2)
            player1 = gr.HTML(make_player(None))
            audio1  = gr.Audio(label='Download Audio', type='filepath')
            btn1.click(fn=generate_from_text, inputs=[text_input, name1], outputs=[audio1, status1, player1, hf1])

        with gr.Tab('πŸ“  Upload .txt File'):
            with gr.Row():
                with gr.Column(scale=2):
                    file_input = gr.File(label='Upload .txt file', file_types=['.txt'])
                    name2 = gr.Textbox(label='File Name (optional)', placeholder='e.g. chapter_102 β€” leave blank for auto name')
                    btn2       = gr.Button('πŸŽ™οΈ Generate Audio', elem_classes='btn-gen')
                with gr.Column(scale=1):
                    gr.HTML('<div class="tip">β€’ Upload any .txt file<br>β€’ Live progress + ETA<br>β€’ Auto saves to HF Dataset forever<br>β€’ Speed control 0.75x–2x</div>')
                    status2 = gr.Textbox(label='Progress', interactive=False, lines=3)
                    hf2     = gr.Textbox(label='Storage Status', interactive=False, lines=2)
            player2 = gr.HTML(make_player(None))
            audio2  = gr.Audio(label='Download Audio', type='filepath')
            btn2.click(fn=generate_from_file, inputs=[file_input, name2], outputs=[audio2, status2, player2, hf2])

demo.launch()