multija commited on
Commit
1f065b7
·
verified ·
1 Parent(s): c02f75f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -168
app.py CHANGED
@@ -1,187 +1,78 @@
1
  import os
2
- import sys
3
- import time
4
- import base64
5
  import logging
6
  import traceback
7
- from inspect import signature
8
 
9
- import requests
10
- import soundfile as sf
11
- import numpy as np
12
-
13
- from fastapi import FastAPI, Body, HTTPException
14
- from fastapi.responses import FileResponse
15
-
16
- # Se a instalação via pip não expuser f5_tts.inference, forçar src path (já clonamos no Dockerfile)
17
- # sys.path.append("/F5-TTS/src") # se precisar forçar, descomente
18
-
19
- # Tenta importar inference de forma robusta
20
- try:
21
- # preferência: f5_tts.inference (de repo)
22
- from f5_tts.inference import inference
23
- except Exception:
24
- # fallback: tentar importar o pacote instalado
25
- try:
26
- from f5_tts.api import inference # caso esteja exposto assim
27
- except Exception:
28
- inference = None
29
-
30
- # --- Config ---
31
  logging.basicConfig(level=logging.INFO)
32
- log = logging.getLogger("f5tts-api")
33
 
34
- MODEL_URL = "https://huggingface.co/firstpixel/F5-TTS-pt-br/resolve/main/pt-br/model_last.safetensors"
35
- CONFIG_URL = "https://raw.githubusercontent.com/SWivid/F5-TTS/refs/heads/main/src/f5_tts/configs/F5TTS_Base.yaml"
36
- VOCAB_URL = "https://huggingface.co/SWivid/F5-TTS/raw/main/F5TTS_Base/vocab.txt"
37
 
38
- MODEL_FILE = "model/model_last.safetensors"
39
- CONFIG_FILE = "config/config.yaml"
40
- VOCAB_FILE = "vocab/vocab.txt"
41
- OUTPUT_FILE = "output/output.wav"
42
-
43
- os.makedirs("model", exist_ok=True)
44
- os.makedirs("config", exist_ok=True)
45
- os.makedirs("vocab", exist_ok=True)
46
- os.makedirs("output", exist_ok=True)
47
-
48
- def download_file(url, dest_path, retry=2):
49
- if os.path.exists(dest_path):
50
- log.info(f"Arquivo já existe: {dest_path}")
51
- return dest_path
52
- for attempt in range(1, retry+1):
53
  try:
54
- log.info(f"Baixando {url} -> {dest_path} (tentativa {attempt})")
55
- r = requests.get(url, stream=True, timeout=120)
56
- r.raise_for_status()
57
- with open(dest_path, "wb") as f:
58
- for chunk in r.iter_content(chunk_size=8192):
59
- if chunk:
60
- f.write(chunk)
61
- log.info(f"Download concluído: {dest_path}")
62
- return dest_path
63
- except Exception as e:
64
- log.warning(f"Falha download ({attempt}/{retry}): {e}")
65
- if attempt == retry:
 
 
 
 
 
 
 
 
 
 
 
 
66
  raise
67
- time.sleep(3)
68
- return dest_path
69
-
70
- # baixar se necessário
71
- try:
72
- download_file(MODEL_URL, MODEL_FILE)
73
- download_file(CONFIG_URL, CONFIG_FILE)
74
- download_file(VOCAB_URL, VOCAB_FILE)
75
- except Exception as e:
76
- log.error("Erro ao baixar arquivos iniciais: %s", e)
77
- log.debug(traceback.format_exc())
78
- # não aborta a inicialização - se o modelo for provido manualmente, ainda pode funcionar
79
 
80
- # Certifica que temos a função inference
81
- if inference is None:
82
- # tentativa de importar novamente via sys.path ajustado (se o repo estiver copiado)
83
- try:
84
- sys.path.append("/F5-TTS/src")
85
- from f5_tts.inference import inference
86
- log.info("Import f5_tts.inference via /F5-TTS/src OK")
87
- except Exception:
88
- log.error("Não foi possível importar f5_tts.inference. Verifique instalação do F5-TTS.")
89
- log.debug(traceback.format_exc())
90
- raise RuntimeError("inference function not available (f5_tts)")
91
 
92
- # Detecta dispositivo disponível (por segurança usa CPU se não tiver CUDA)
93
- try:
94
- import torch
95
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
96
- except Exception:
97
- DEVICE = "cpu"
98
 
99
- log.info(f"Device selecionado: {DEVICE}")
 
 
 
100
 
101
- # Função genérica que adapta kwargs à assinatura da função inference
102
- def call_inference_dynamic(text, model_path, config_path, vocab_path, device):
103
- sig = signature(inference)
104
- params = sig.parameters.keys()
105
- # montar mapping de possíveis nomes usados no repo
106
- candidate_kwargs = {
107
- "text": text,
108
- "model_path": model_path,
109
- "config_path": config_path,
110
- "vocab_path": vocab_path,
111
- "vocab_file": vocab_path,
112
- "vocab": vocab_path,
113
- "model_file": model_path,
114
- "config_file": config_path,
115
- "device": device,
116
- "use_cuda": True if device == "cuda" else False,
117
- }
118
- # escolher apenas keys que a função aceita
119
- kwargs = {k: v for k, v in candidate_kwargs.items() if k in params}
120
- log.debug(f"Chamando inference com args: {list(kwargs.keys())}")
121
- result = inference(**kwargs)
122
- return result
123
-
124
- # helper para normalizar retorno
125
- def normalize_inference_output(result):
126
- """
127
- Pode retornar:
128
- - numpy array (waveform) -> assume sr 24000
129
- - tuple (waveform, sr)
130
- - list-like
131
- """
132
- if result is None:
133
- raise RuntimeError("Inference retornou None")
134
- # se for tuple (audio, sr)
135
- if isinstance(result, tuple) or isinstance(result, list):
136
- if len(result) >= 2 and isinstance(result[0], (list, tuple, np.ndarray)):
137
- audio = np.asarray(result[0], dtype=np.float32)
138
- sr = int(result[1])
139
- return audio, sr
140
- # se for numpy array ou list
141
- if isinstance(result, np.ndarray):
142
- return result, 24000
143
- if isinstance(result, (list, tuple)):
144
- arr = np.asarray(result, dtype=np.float32)
145
- return arr, 24000
146
- # caso diferente
147
- raise RuntimeError("Formato de retorno inesperado da inference()")
148
 
149
- # inicializar FastAPI
150
- app = FastAPI(title="F5-TTS Minimal API")
151
 
 
 
152
  @app.get("/health")
153
  def health():
154
- return {"status": "ok", "device": DEVICE}
155
 
 
156
  @app.post("/tts")
157
- def tts_endpoint(text: str = Body(..., embed=True), return_base64: bool = Body(False, embed=True)):
158
- """
159
- Recebe JSON { "text": "...", "return_base64": false }.
160
- Gera output/output.wav e retorna FileResponse (ou base64 se pedir).
161
- """
162
- if not text or not text.strip():
163
- raise HTTPException(status_code=400, detail="Campo 'text' vazio.")
164
- try:
165
- log.info("Gerando áudio para texto (len=%d)", len(text))
166
- result = call_inference_dynamic(
167
- text=text,
168
- model_path=MODEL_FILE,
169
- config_path=CONFIG_FILE,
170
- vocab_path=VOCAB_FILE,
171
- device=DEVICE
172
- )
173
- audio, sr = normalize_inference_output(result)
174
- # salvar wav
175
- out_path = OUTPUT_FILE
176
- sf.write(out_path, audio, sr)
177
- log.info("Arquivo salvo: %s (sr=%d, samples=%d)", out_path, sr, audio.shape[0])
178
- if return_base64:
179
- with open(out_path, "rb") as f:
180
- b = base64.b64encode(f.read()).decode("ascii")
181
- return {"filename": os.path.basename(out_path), "audio_base64": b}
182
- # retorna arquivo
183
- return FileResponse(out_path, media_type="audio/wav", filename=os.path.basename(out_path))
184
- except Exception as e:
185
- log.error("Erro na geração TTS: %s", e)
186
- log.debug(traceback.format_exc())
187
- raise HTTPException(status_code=500, detail=str(e))
 
1
  import os
 
 
 
2
  import logging
3
  import traceback
 
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  logging.basicConfig(level=logging.INFO)
6
+ log = logging.getLogger("app")
7
 
8
+ # caminhos preferidos
9
+ PREFERRED_DIRS = ["model", "config", "vocab", "output"]
 
10
 
11
+ def ensure_dirs(base_dirs=None):
12
+ base_dirs = base_dirs or PREFERRED_DIRS
13
+ created = {}
14
+ for d in base_dirs:
 
 
 
 
 
 
 
 
 
 
 
15
  try:
16
+ os.makedirs(d, exist_ok=True)
17
+ # testar escrita
18
+ testfile = os.path.join(d, ".perm_test")
19
+ with open(testfile, "w") as f:
20
+ f.write("ok")
21
+ os.remove(testfile)
22
+ created[d] = d
23
+ log.info("Diretório pronto: %s", d)
24
+ except PermissionError:
25
+ log.warning("Sem permissão para criar %s", d)
26
+ created[d] = None
27
+ except Exception:
28
+ log.error("Erro criando %s: %s", d, traceback.format_exc())
29
+ created[d] = None
30
+ # se algum não foi criado, usar /tmp/<nome>
31
+ for k, v in list(created.items()):
32
+ if v is None:
33
+ alt = os.path.join("/tmp", k)
34
+ try:
35
+ os.makedirs(alt, exist_ok=True)
36
+ created[k] = alt
37
+ log.info("Usando fallback %s para %s", alt, k)
38
+ except Exception:
39
+ log.error("Não foi possível criar fallback %s", alt)
40
  raise
41
+ return created
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ # chama no início
44
+ DIRS = ensure_dirs()
 
 
 
 
 
 
 
 
 
45
 
46
+ MODEL_DIR = DIRS["model"]
47
+ CONFIG_DIR = DIRS["config"]
48
+ VOCAB_DIR = DIRS["vocab"]
49
+ OUTPUT_DIR = DIRS["output"]
 
 
50
 
51
+ MODEL_FILE = os.path.join(MODEL_DIR, "model_last.safetensors")
52
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yaml")
53
+ VOCAB_FILE = os.path.join(VOCAB_DIR, "vocab.txt")
54
+ OUTPUT_FILE = os.path.join(OUTPUT_DIR, "output.wav")
55
 
56
+ # segue resto do seu app (import fastapi etc.)
57
+ from fastapi import FastAPI, Body, HTTPException
58
+ from fastapi.responses import FileResponse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ app = FastAPI()
 
61
 
62
+ # ... (coloque aqui o seu código que faz download do modelo / inicializa TTS etc.)
63
+ # Exemplo simples de endpoint:
64
  @app.get("/health")
65
  def health():
66
+ return {"status": "ok", "paths": {"model": MODEL_FILE, "output": OUTPUT_FILE}}
67
 
68
+ # Endpoint /tts de exemplo (adapte para sua inference)
69
  @app.post("/tts")
70
+ def tts(text: str = Body(..., embed=True)):
71
+ if not text.strip():
72
+ raise HTTPException(status_code=400, detail="text vazio")
73
+ # aqui você chama sua função de inferência que escreve OUTPUT_FILE
74
+ # por exemplo: tts_model.tts_to_file(text=text, file_path=OUTPUT_FILE)
75
+ # para demo, apenas cria um arquivo vazio (substitua isso)
76
+ with open(OUTPUT_FILE, "wb") as f:
77
+ f.write(b"") # <-- substitua pelo áudio real
78
+ return FileResponse(OUTPUT_FILE, media_type="audio/wav", filename=os.path.basename(OUTPUT_FILE))