Spaces:
Sleeping
Sleeping
File size: 9,140 Bytes
0ab4863 8bd3db2 6643d73 8bd3db2 0ab4863 ade3276 030267f 4b6c3b6 9204f14 030267f 0ab4863 9204f14 ade3276 b3491aa ade3276 b3491aa ade3276 b3491aa ade3276 b3491aa ade3276 c3b1277 4b6c3b6 5cb7294 8bd3db2 5cb7294 8bd3db2 5cb7294 c0f81fb 56028dd 32116b4 56028dd c0f81fb 60708e8 56028dd c0f81fb 60708e8 c0f81fb e345d4f c0f81fb e345d4f c0f81fb e345d4f c0f81fb e345d4f c0f81fb 56028dd 60708e8 5cb7294 030267f 8bd3db2 4b6c3b6 33cf2ec b3491aa 33cf2ec ade3276 8bd3db2 ade3276 8bd3db2 ade3276 0ab4863 4b6c3b6 | 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 | """
cantrell-kokoro-engine β StoryVoice Cloning Backend
Docker Space, Python 3.12, FastAPI only
KokoClone zero-shot voice cloning via Kanade voice conversion
"""
import os
import sys
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ββ KokoClone voice cloning (lazy-loaded) ββββββββββββββββββββββββββββββββββββ
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
VOICES_DIR = os.path.join(BASE_DIR, "voices")
sys.path.insert(0, BASE_DIR)
_kokoclone = None
def get_kokoclone():
global _kokoclone
if _kokoclone is None:
from core.cloner import KokoClone
print("[StoryVoice] Loading KokoClone...")
_kokoclone = KokoClone()
print("[StoryVoice] KokoClone ready.")
return _kokoclone
def find_voice_audio(voice_id: str):
"""Find a reference MP3/WAV in the Space's voices/ folder."""
voice_id_clean = voice_id.replace(".mp3", "").replace(".wav", "").strip()
for ext in (".mp3", ".wav"):
p = os.path.join(VOICES_DIR, f"{voice_id_clean}{ext}")
if os.path.exists(p):
return p
if os.path.isdir(VOICES_DIR):
for f in os.listdir(VOICES_DIR):
name, ext = os.path.splitext(f)
if ext.lower() in (".mp3", ".wav") and name.lower() == voice_id_clean.lower():
return os.path.join(VOICES_DIR, f)
return None
# ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.get("/")
def index():
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Nyako StoryVoiceβ’</title>
<style>
*{box-sizing:border-box;}
body{margin:0;background:#faf3e6;min-height:100vh;font-family:Georgia,serif;display:flex;align-items:center;justify-content:center;padding:20px;}
.card{background:#fffdf8;border:1px solid #d9b968;border-radius:16px;padding:40px;width:100%;max-width:560px;box-shadow:0 4px 24px rgba(122,38,56,.08);}
h1{color:#7a2638;font-size:20px;letter-spacing:.08em;text-transform:uppercase;margin:0 0 4px;text-align:center;}
.sub{color:#9c8060;font-size:13px;font-style:italic;text-align:center;margin-bottom:16px;}
label{display:block;color:#a8842f;font-size:11px;letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;}
textarea{width:100%;background:#fffdf8;border:1px solid #e6d3a3;border-radius:8px;color:#3a2b1a;font-family:Georgia,serif;font-size:14px;padding:12px;resize:vertical;min-height:100px;outline:none;margin-bottom:16px;}
select{width:100%;background:#fffdf8;border:1px solid #e6d3a3;border-radius:8px;color:#3a2b1a;font-size:14px;padding:10px 12px;outline:none;margin-bottom:16px;appearance:none;}
button{width:100%;background:#7a2638;border:none;color:#fff;font-family:Georgia,serif;font-size:14px;letter-spacing:.06em;text-transform:uppercase;padding:14px;border-radius:8px;cursor:pointer;margin-bottom:16px;}
button:disabled{opacity:.5;cursor:not-allowed;}
audio{width:100%;margin-top:4px;}
.status{text-align:center;color:#4caf50;font-size:12px;letter-spacing:.04em;}
</style>
</head>
<body>
<div class="card">
<h1>Nyako StoryVoiceβ’</h1>
<div class="sub">Voice Clone β Cantrell Creatives</div>
<label>1. Text to Synthesize</label>
<textarea id="clone-txt" rows="8">Welcome to Cantrell Creatives. This is where creativity lives β where your characters speak, your stories breathe, and your voice is finally heard.</textarea>
<label>2. Your Voice</label>
<select id="clone-voice"></select>
<button id="clone-btn" onclick="generateClone()">π Generate Clone</button>
<audio id="clone-player" controls style="display:none"></audio>
<div class="status" id="clone-status"></div>
</div>
<script>
fetch('/my-voices').then(r=>r.json()).then(function(data){
var sel = document.getElementById('clone-voice');
data.forEach(function(v){
var o = document.createElement('option');
o.value = v.voice_id;
o.textContent = v.display_name;
sel.appendChild(o);
});
});
function generateClone(){
var btn = document.getElementById('clone-btn');
var status = document.getElementById('clone-status');
var player = document.getElementById('clone-player');
btn.disabled = true;
btn.textContent = 'Cloning...';
status.textContent = '';
player.style.display = 'none';
var startTime = Date.now();
var timer = setInterval(function(){
status.style.color = '#c9a040';
status.textContent = 'β± ' + ((Date.now()-startTime)/1000).toFixed(1) + 's';
}, 100);
fetch('/clone',{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
text: document.getElementById('clone-txt').value,
voice_id: document.getElementById('clone-voice').value,
lang: 'en'
})
})
.then(function(r){ if(!r.ok) throw new Error('Clone failed'); return r.blob(); })
.then(function(blob){
clearInterval(timer);
var genTime = ((Date.now()-startTime)/1000).toFixed(1);
player.src = URL.createObjectURL(blob);
player.style.display = 'block';
player.onloadedmetadata = function(){
status.style.color = '#4caf50';
status.textContent = 'β Ready β generated in ' + genTime + 's, clip length ' + player.duration.toFixed(1) + 's';
};
player.play();
})
.catch(function(e){
clearInterval(timer);
status.style.color='#e74c3c';
status.textContent = 'Error: ' + e.message;
})
.finally(function(){
btn.disabled = false;
btn.textContent = 'Generate Clone';
});
}
</script>
</body>
</html>"""
return Response(content=html, media_type="text/html")
@app.get("/health")
def health():
return {"status": "ok", "engine": "kokoclone"}
@app.get("/my-voices")
def my_voices():
"""Returns list of custom voice MP3s available for cloning."""
found = []
if os.path.isdir(VOICES_DIR):
for f in sorted(os.listdir(VOICES_DIR)):
name, ext = os.path.splitext(f)
if ext.lower() in (".mp3", ".wav"):
found.append({"voice_id": name, "display_name": name, "file": f})
return found
# ββ KokoClone endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class CloneRequest(BaseModel):
text: str
voice_id: str # name of your reference MP3 in the voices/ folder
speed: float = 1.0
lang: str = "en"
@app.post("/clone")
def clone(req: CloneRequest):
"""Generate speech cloned to match a reference voice MP3."""
if not req.text.strip():
raise HTTPException(status_code=400, detail="text is required")
ref_path = find_voice_audio(req.voice_id)
if not ref_path:
raise HTTPException(status_code=404, detail=f"Voice reference not found: {req.voice_id}")
try:
import tempfile
cloner = get_kokoclone()
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
out_path = tmp.name
cloner.generate(
text=req.text.strip(),
lang=req.lang,
reference_audio=ref_path,
output_path=out_path
)
with open(out_path, "rb") as f:
audio_bytes = f.read()
os.remove(out_path)
return Response(content=audio_bytes, media_type="audio/wav")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
class ConvertRequest(BaseModel):
voice_id: str # target reference voice MP3
source_audio_b64: str # base64-encoded source WAV/MP3
@app.post("/convert")
def convert(req: ConvertRequest):
"""Re-voice existing audio to match a reference voice MP3."""
import base64, tempfile
ref_path = find_voice_audio(req.voice_id)
if not ref_path:
raise HTTPException(status_code=404, detail=f"Voice reference not found: {req.voice_id}")
try:
cloner = get_kokoclone()
src_bytes = base64.b64decode(req.source_audio_b64)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as src_tmp:
src_tmp.write(src_bytes)
src_path = src_tmp.name
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as out_tmp:
out_path = out_tmp.name
cloner.convert(
source_audio=src_path,
reference_audio=ref_path,
output_path=out_path
)
with open(out_path, "rb") as f:
audio_bytes = f.read()
os.remove(src_path)
os.remove(out_path)
return Response(content=audio_bytes, media_type="audio/wav")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=7860) |