mirror of
https://github.com/remsky/Kokoro-FastAPI.git
synced 2025-08-05 16:48:53 +00:00
WIP: Add Gradio interface for Kokoro TTS application with input, model, and output components
This commit is contained in:
parent
a672fbc798
commit
1163beae3a
14 changed files with 712 additions and 0 deletions
15
ui/Dockerfile
Normal file
15
ui/Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app/ui
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install gradio==5.9.1 requests==2.32.3
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p data/inputs data/outputs
|
||||||
|
|
||||||
|
# Copy the application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run the Gradio app
|
||||||
|
CMD ["python", "app.py"]
|
BIN
ui/GUIBanner.png
Normal file
BIN
ui/GUIBanner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 486 KiB |
9
ui/app.py
Normal file
9
ui/app.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from lib.interface import create_interface
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demo = create_interface()
|
||||||
|
demo.launch(
|
||||||
|
server_name="0.0.0.0",
|
||||||
|
server_port=7860,
|
||||||
|
show_error=True
|
||||||
|
)
|
151
ui/data/inputs/test_timemachine.txt
Normal file
151
ui/data/inputs/test_timemachine.txt
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
The Time Traveller (for so it will be convenient to speak of him) was expounding a recondite matter to us. His pale grey eyes shone and twinkled, and his usually pale face was flushed and animated. The fire burnt brightly, and the soft radiance of the incandescent lights in the lilies of silver caught the bubbles that flashed and passed in our glasses. Our chairs, being his patents, embraced and caressed us rather than submitted to be sat upon, and there was that luxurious after-dinner atmosphere, when thought runs gracefully free of the trammels of precision. And he put it to us in this way—marking the points with a lean forefinger—as we sat and lazily admired his earnestness over this new paradox (as we thought it) and his fecundity.
|
||||||
|
|
||||||
|
“You must follow me carefully. I shall have to controvert one or two ideas that are almost universally accepted. The geometry, for instance, they taught you at school is founded on a misconception.”
|
||||||
|
|
||||||
|
“Is not that rather a large thing to expect us to begin upon?” said Filby, an argumentative person with red hair.
|
||||||
|
|
||||||
|
“I do not mean to ask you to accept anything without reasonable ground for it. You will soon admit as much as I need from you. You know of course that a mathematical line, a line of thickness nil, has no real existence. They taught you that? Neither has a mathematical plane. These things are mere abstractions.”
|
||||||
|
|
||||||
|
“That is all right,” said the Psychologist.
|
||||||
|
|
||||||
|
“Nor, having only length, breadth, and thickness, can a cube have a real existence.”
|
||||||
|
|
||||||
|
“There I object,” said Filby. “Of course a solid body may exist. All real things—”
|
||||||
|
|
||||||
|
“So most people think. But wait a moment. Can an instantaneous cube exist?”
|
||||||
|
|
||||||
|
“Don’t follow you,” said Filby.
|
||||||
|
|
||||||
|
“Can a cube that does not last for any time at all, have a real existence?”
|
||||||
|
|
||||||
|
Filby became pensive. “Clearly,” the Time Traveller proceeded, “any real body must have extension in four directions: it must have Length, Breadth, Thickness, and—Duration. But through a natural infirmity of the flesh, which I will explain to you in a moment, we incline to overlook this fact. There are really four dimensions, three which we call the three planes of Space, and a fourth, Time. There is, however, a tendency to draw an unreal distinction between the former three dimensions and the latter, because it happens that our consciousness moves intermittently in one direction along the latter from the beginning to the end of our lives.”
|
||||||
|
|
||||||
|
“That,” said a very young man, making spasmodic efforts to relight his cigar over the lamp; “that . . . very clear indeed.”
|
||||||
|
|
||||||
|
“Now, it is very remarkable that this is so extensively overlooked,” continued the Time Traveller, with a slight accession of cheerfulness. “Really this is what is meant by the Fourth Dimension, though some people who talk about the Fourth Dimension do not know they mean it. It is only another way of looking at Time. There is no difference between Time and any of the three dimensions of Space except that our consciousness moves along it. But some foolish people have got hold of the wrong side of that idea. You have all heard what they have to say about this Fourth Dimension?”
|
||||||
|
|
||||||
|
“I have not,” said the Provincial Mayor.
|
||||||
|
|
||||||
|
“It is simply this. That Space, as our mathematicians have it, is spoken of as having three dimensions, which one may call Length, Breadth, and Thickness, and is always definable by reference to three planes, each at right angles to the others. But some philosophical people have been asking why three dimensions particularly—why not another direction at right angles to the other three?—and have even tried to construct a Four-Dimensional geometry. Professor Simon Newcomb was expounding this to the New York Mathematical Society only a month or so ago. You know how on a flat surface, which has only two dimensions, we can represent a figure of a three-dimensional solid, and similarly they think that by models of three dimensions they could represent one of four—if they could master the perspective of the thing. See?”
|
||||||
|
|
||||||
|
“I think so,” murmured the Provincial Mayor; and, knitting his brows, he lapsed into an introspective state, his lips moving as one who repeats mystic words. “Yes, I think I see it now,” he said after some time, brightening in a quite transitory manner.
|
||||||
|
|
||||||
|
“Well, I do not mind telling you I have been at work upon this geometry of Four Dimensions for some time. Some of my results are curious. For instance, here is a portrait of a man at eight years old, another at fifteen, another at seventeen, another at twenty-three, and so on. All these are evidently sections, as it were, Three-Dimensional representations of his Four-Dimensioned being, which is a fixed and unalterable thing.
|
||||||
|
|
||||||
|
“Scientific people,” proceeded the Time Traveller, after the pause required for the proper assimilation of this, “know very well that Time is only a kind of Space. Here is a popular scientific diagram, a weather record. This line I trace with my finger shows the movement of the barometer. Yesterday it was so high, yesterday night it fell, then this morning it rose again, and so gently upward to here. Surely the mercury did not trace this line in any of the dimensions of Space generally recognised? But certainly it traced such a line, and that line, therefore, we must conclude, was along the Time-Dimension.”
|
||||||
|
|
||||||
|
“But,” said the Medical Man, staring hard at a coal in the fire, “if Time is really only a fourth dimension of Space, why is it, and why has it always been, regarded as something different? And why cannot we move in Time as we move about in the other dimensions of Space?”
|
||||||
|
|
||||||
|
The Time Traveller smiled. “Are you so sure we can move freely in Space? Right and left we can go, backward and forward freely enough, and men always have done so. I admit we move freely in two dimensions. But how about up and down? Gravitation limits us there.”
|
||||||
|
|
||||||
|
“Not exactly,” said the Medical Man. “There are balloons.”
|
||||||
|
|
||||||
|
“But before the balloons, save for spasmodic jumping and the inequalities of the surface, man had no freedom of vertical movement.”
|
||||||
|
|
||||||
|
“Still they could move a little up and down,” said the Medical Man.
|
||||||
|
|
||||||
|
“Easier, far easier down than up.”
|
||||||
|
|
||||||
|
“And you cannot move at all in Time, you cannot get away from the present moment.”
|
||||||
|
|
||||||
|
“My dear sir, that is just where you are wrong. That is just where the whole world has gone wrong. We are always getting away from the present moment. Our mental existences, which are immaterial and have no dimensions, are passing along the Time-Dimension with a uniform velocity from the cradle to the grave. Just as we should travel down if we began our existence fifty miles above the earth’s surface.”
|
||||||
|
|
||||||
|
“But the great difficulty is this,” interrupted the Psychologist. ’You can move about in all directions of Space, but you cannot move about in Time.”
|
||||||
|
|
||||||
|
“That is the germ of my great discovery. But you are wrong to say that we cannot move about in Time. For instance, if I am recalling an incident very vividly I go back to the instant of its occurrence: I become absent-minded, as you say. I jump back for a moment. Of course we have no means of staying back for any length of Time, any more than a savage or an animal has of staying six feet above the ground. But a civilised man is better off than the savage in this respect. He can go up against gravitation in a balloon, and why should he not hope that ultimately he may be able to stop or accelerate his drift along the Time-Dimension, or even turn about and travel the other way?”
|
||||||
|
|
||||||
|
“Oh, this,” began Filby, “is all—”
|
||||||
|
|
||||||
|
“Why not?” said the Time Traveller.
|
||||||
|
|
||||||
|
“It’s against reason,” said Filby.
|
||||||
|
|
||||||
|
“What reason?” said the Time Traveller.
|
||||||
|
|
||||||
|
“You can show black is white by argument,” said Filby, “but you will never convince me.”
|
||||||
|
|
||||||
|
“Possibly not,” said the Time Traveller. “But now you begin to see the object of my investigations into the geometry of Four Dimensions. Long ago I had a vague inkling of a machine—”
|
||||||
|
|
||||||
|
“To travel through Time!” exclaimed the Very Young Man.
|
||||||
|
|
||||||
|
“That shall travel indifferently in any direction of Space and Time, as the driver determines.”
|
||||||
|
|
||||||
|
Filby contented himself with laughter.
|
||||||
|
|
||||||
|
“But I have experimental verification,” said the Time Traveller.
|
||||||
|
|
||||||
|
“It would be remarkably convenient for the historian,” the Psychologist suggested. “One might travel back and verify the accepted account of the Battle of Hastings, for instance!”
|
||||||
|
|
||||||
|
“Don’t you think you would attract attention?” said the Medical Man. “Our ancestors had no great tolerance for anachronisms.”
|
||||||
|
|
||||||
|
“One might get one’s Greek from the very lips of Homer and Plato,” the Very Young Man thought.
|
||||||
|
|
||||||
|
“In which case they would certainly plough you for the Little-go. The German scholars have improved Greek so much.”
|
||||||
|
|
||||||
|
“Then there is the future,” said the Very Young Man. “Just think! One might invest all one’s money, leave it to accumulate at interest, and hurry on ahead!”
|
||||||
|
|
||||||
|
“To discover a society,” said I, “erected on a strictly communistic basis.”
|
||||||
|
|
||||||
|
“Of all the wild extravagant theories!” began the Psychologist.
|
||||||
|
|
||||||
|
“Yes, so it seemed to me, and so I never talked of it until—”
|
||||||
|
|
||||||
|
“Experimental verification!” cried I. “You are going to verify that?”
|
||||||
|
|
||||||
|
“The experiment!” cried Filby, who was getting brain-weary.
|
||||||
|
|
||||||
|
“Let’s see your experiment anyhow,” said the Psychologist, “though it’s all humbug, you know.”
|
||||||
|
|
||||||
|
The Time Traveller smiled round at us. Then, still smiling faintly, and with his hands deep in his trousers pockets, he walked slowly out of the room, and we heard his slippers shuffling down the long passage to his laboratory.
|
||||||
|
|
||||||
|
The Psychologist looked at us. “I wonder what he’s got?”
|
||||||
|
|
||||||
|
“Some sleight-of-hand trick or other,” said the Medical Man, and Filby tried to tell us about a conjuror he had seen at Burslem, but before he had finished his preface the Time Traveller came back, and Filby’s anecdote collapsed.
|
||||||
|
|
||||||
|
II.
|
||||||
|
The Machine
|
||||||
|
The thing the Time Traveller held in his hand was a glittering metallic framework, scarcely larger than a small clock, and very delicately made. There was ivory in it, and some transparent crystalline substance. And now I must be explicit, for this that follows—unless his explanation is to be accepted—is an absolutely unaccountable thing. He took one of the small octagonal tables that were scattered about the room, and set it in front of the fire, with two legs on the hearthrug. On this table he placed the mechanism. Then he drew up a chair, and sat down. The only other object on the table was a small shaded lamp, the bright light of which fell upon the model. There were also perhaps a dozen candles about, two in brass candlesticks upon the mantel and several in sconces, so that the room was brilliantly illuminated. I sat in a low arm-chair nearest the fire, and I drew this forward so as to be almost between the Time Traveller and the fireplace. Filby sat behind him, looking over his shoulder. The Medical Man and the Provincial Mayor watched him in profile from the right, the Psychologist from the left. The Very Young Man stood behind the Psychologist. We were all on the alert. It appears incredible to me that any kind of trick, however subtly conceived and however adroitly done, could have been played upon us under these conditions.
|
||||||
|
|
||||||
|
The Time Traveller looked at us, and then at the mechanism. “Well?” said the Psychologist.
|
||||||
|
|
||||||
|
“This little affair,” said the Time Traveller, resting his elbows upon the table and pressing his hands together above the apparatus, “is only a model. It is my plan for a machine to travel through time. You will notice that it looks singularly askew, and that there is an odd twinkling appearance about this bar, as though it was in some way unreal.” He pointed to the part with his finger. “Also, here is one little white lever, and here is another.”
|
||||||
|
|
||||||
|
The Medical Man got up out of his chair and peered into the thing. “It’s beautifully made,” he said.
|
||||||
|
|
||||||
|
“It took two years to make,” retorted the Time Traveller. Then, when we had all imitated the action of the Medical Man, he said: “Now I want you clearly to understand that this lever, being pressed over, sends the machine gliding into the future, and this other reverses the motion. This saddle represents the seat of a time traveller. Presently I am going to press the lever, and off the machine will go. It will vanish, pass into future Time, and disappear. Have a good look at the thing. Look at the table too, and satisfy yourselves there is no trickery. I don’t want to waste this model, and then be told I’m a quack.”
|
||||||
|
|
||||||
|
There was a minute’s pause perhaps. The Psychologist seemed about to speak to me, but changed his mind. Then the Time Traveller put forth his finger towards the lever. “No,” he said suddenly. “Lend me your hand.” And turning to the Psychologist, he took that individual’s hand in his own and told him to put out his forefinger. So that it was the Psychologist himself who sent forth the model Time Machine on its interminable voyage. We all saw the lever turn. I am absolutely certain there was no trickery. There was a breath of wind, and the lamp flame jumped. One of the candles on the mantel was blown out, and the little machine suddenly swung round, became indistinct, was seen as a ghost for a second perhaps, as an eddy of faintly glittering brass and ivory; and it was gone—vanished! Save for the lamp the table was bare.
|
||||||
|
|
||||||
|
Everyone was silent for a minute. Then Filby said he was damned.
|
||||||
|
|
||||||
|
The Psychologist recovered from his stupor, and suddenly looked under the table. At that the Time Traveller laughed cheerfully. “Well?” he said, with a reminiscence of the Psychologist. Then, getting up, he went to the tobacco jar on the mantel, and with his back to us began to fill his pipe.
|
||||||
|
|
||||||
|
We stared at each other. “Look here,” said the Medical Man, “are you in earnest about this? Do you seriously believe that that machine has travelled into time?”
|
||||||
|
|
||||||
|
“Certainly,” said the Time Traveller, stooping to light a spill at the fire. Then he turned, lighting his pipe, to look at the Psychologist’s face. (The Psychologist, to show that he was not unhinged, helped himself to a cigar and tried to light it uncut.) “What is more, I have a big machine nearly finished in there”—he indicated the laboratory—“and when that is put together I mean to have a journey on my own account.”
|
||||||
|
|
||||||
|
“You mean to say that that machine has travelled into the future?” said Filby.
|
||||||
|
|
||||||
|
“Into the future or the past—I don’t, for certain, know which.”
|
||||||
|
|
||||||
|
After an interval the Psychologist had an inspiration. “It must have gone into the past if it has gone anywhere,” he said.
|
||||||
|
|
||||||
|
“Why?” said the Time Traveller.
|
||||||
|
|
||||||
|
“Because I presume that it has not moved in space, and if it travelled into the future it would still be here all this time, since it must have travelled through this time.”
|
||||||
|
|
||||||
|
“But,” said I, “If it travelled into the past it would have been visible when we came first into this room; and last Thursday when we were here; and the Thursday before that; and so forth!”
|
||||||
|
|
||||||
|
“Serious objections,” remarked the Provincial Mayor, with an air of impartiality, turning towards the Time Traveller.
|
||||||
|
|
||||||
|
“Not a bit,” said the Time Traveller, and, to the Psychologist: “You think. You can explain that. It’s presentation below the threshold, you know, diluted presentation.”
|
||||||
|
|
||||||
|
“Of course,” said the Psychologist, and reassured us. “That’s a simple point of psychology. I should have thought of it. It’s plain enough, and helps the paradox delightfully. We cannot see it, nor can we appreciate this machine, any more than we can the spoke of a wheel spinning, or a bullet flying through the air. If it is travelling through time fifty times or a hundred times faster than we are, if it gets through a minute while we get through a second, the impression it creates will of course be only one-fiftieth or one-hundredth of what it would make if it were not travelling in time. That’s plain enough.” He passed his hand through the space in which the machine had been. “You see?” he said, laughing.
|
||||||
|
|
||||||
|
We sat and stared at the vacant table for a minute or so. Then the Time Traveller asked us what we thought of it all.
|
||||||
|
|
||||||
|
“It sounds plausible enough tonight,” said the Medical Man; “but wait until tomorrow. Wait for the common sense of the morning.”
|
||||||
|
|
||||||
|
“Would you like to see the Time Machine itself?” asked the Time Traveller. And therewith, taking the lamp in his hand, he led the way down the long, draughty corridor to his laboratory. I remember vividly the flickering light, his queer, broad head in silhouette, the dance of the shadows, how we all followed him, puzzled but incredulous, and how there in the laboratory we beheld a larger edition of the little mechanism which we had seen vanish from before our eyes. Parts were of nickel, parts of ivory, parts had certainly been filed or sawn out of rock crystal. The thing was generally complete, but the twisted crystalline bars lay unfinished upon the bench beside some
|
||||||
|
The Time Traveller Returns
|
||||||
|
I think that at that time none of us quite believed in the Time Machine. The fact is, the Time Traveller was one of those men who are too clever to be believed: you never felt that you saw all round him; you always suspected some subtle reserve, some ingenuity in ambush, behind his lucid frankness. Had Filby shown the model and explained the matter in the Time Traveller’s words, we should have shown him far less scepticism. For we should have perceived his motives: a pork-butcher could understand Filby. But the Time Traveller had more than a touch of whim among his elements, and we distrusted him. Things that would have made the fame of a less clever man seemed tricks in his hands. It is a mistake to do things too easily. The serious people who took him seriously never felt quite sure of his deportment; they were somehow aware that trusting their reputations for judgment with him was like furnishing a nursery with eggshell china. So I don’t think any of us said very much about time travelling in the interval between that Thursday and the next, though its odd potentialities ran, no doubt, in most of our minds: its plausibility, that is, its practical incredibleness, the curious possibilities of anachronism and of utter confusion it suggested. For my own part, I was particularly preoccupied with the trick of the model. That I remember discussing with the Medical Man, whom I met on Friday at the Linnæan. He said he had seen a similar thing at Tübingen, and laid considerable stress on the blowing-out of the candle. But how the trick was done he could not explai
|
0
ui/lib/__init__.py
Normal file
0
ui/lib/__init__.py
Normal file
75
ui/lib/api.py
Normal file
75
ui/lib/api.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import requests
|
||||||
|
from typing import Tuple, List, Optional
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from .config import API_URL, OUTPUTS_DIR
|
||||||
|
|
||||||
|
def check_api_status() -> Tuple[bool, List[str]]:
|
||||||
|
"""Check TTS service status and get available voices."""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_URL}/v1/audio/voices", timeout=5)
|
||||||
|
response.raise_for_status()
|
||||||
|
voices = response.json().get("voices", [])
|
||||||
|
if voices:
|
||||||
|
return True, voices
|
||||||
|
print("No voices found in response")
|
||||||
|
return False, []
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("API request timed out")
|
||||||
|
return False, []
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"API request failed: {str(e)}")
|
||||||
|
return False, []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error checking API status: {str(e)}")
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
def text_to_speech(text: str, voice_id: str, format: str, speed: float) -> Optional[str]:
|
||||||
|
"""Generate speech from text using TTS API."""
|
||||||
|
if not text.strip():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create output filename
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
output_filename = f"output_{timestamp}_voice-{voice_id}_speed-{speed}.{format}"
|
||||||
|
output_path = os.path.join(OUTPUTS_DIR, output_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/v1/audio/speech",
|
||||||
|
json={
|
||||||
|
"model": "kokoro",
|
||||||
|
"input": text,
|
||||||
|
"voice": voice_id,
|
||||||
|
"response_format": format,
|
||||||
|
"speed": float(speed)
|
||||||
|
},
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=300 # Longer timeout for speech generation
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("Speech generation request timed out")
|
||||||
|
return None
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Speech generation request failed: {str(e)}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error generating speech: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_status_html(is_available: bool) -> str:
|
||||||
|
"""Generate HTML for status indicator."""
|
||||||
|
color = "green" if is_available else "red"
|
||||||
|
status = "Available" if is_available else "Unavailable"
|
||||||
|
return f"""
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: {color};"></div>
|
||||||
|
<span>TTS Service: {status}</span>
|
||||||
|
</div>
|
||||||
|
"""
|
5
ui/lib/components/__init__.py
Normal file
5
ui/lib/components/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .input import create_input_column
|
||||||
|
from .model import create_model_column
|
||||||
|
from .output import create_output_column
|
||||||
|
|
||||||
|
__all__ = ['create_input_column', 'create_model_column', 'create_output_column']
|
46
ui/lib/components/input.py
Normal file
46
ui/lib/components/input.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import gradio as gr
|
||||||
|
from typing import Tuple
|
||||||
|
from .. import files
|
||||||
|
|
||||||
|
def create_input_column() -> Tuple[gr.Column, dict]:
|
||||||
|
"""Create the input column with text input and file handling."""
|
||||||
|
with gr.Column(scale=1) as col:
|
||||||
|
with gr.Tabs() as tabs:
|
||||||
|
# Direct Input Tab
|
||||||
|
with gr.TabItem("Direct Input"):
|
||||||
|
text_input = gr.Textbox(
|
||||||
|
label="Text to speak",
|
||||||
|
placeholder="Enter text here...",
|
||||||
|
lines=4
|
||||||
|
)
|
||||||
|
|
||||||
|
# File Input Tab
|
||||||
|
with gr.TabItem("From File"):
|
||||||
|
# Existing files dropdown
|
||||||
|
input_files_list = gr.Dropdown(
|
||||||
|
label="Select Existing File",
|
||||||
|
choices=files.list_input_files(),
|
||||||
|
value=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simple file upload
|
||||||
|
file_upload = gr.File(
|
||||||
|
label="Upload Text File (.txt)",
|
||||||
|
file_types=[".txt"]
|
||||||
|
)
|
||||||
|
|
||||||
|
file_preview = gr.Textbox(
|
||||||
|
label="File Content Preview",
|
||||||
|
interactive=False,
|
||||||
|
lines=4
|
||||||
|
)
|
||||||
|
|
||||||
|
components = {
|
||||||
|
"tabs": tabs,
|
||||||
|
"text_input": text_input,
|
||||||
|
"file_select": input_files_list,
|
||||||
|
"file_upload": file_upload,
|
||||||
|
"file_preview": file_preview
|
||||||
|
}
|
||||||
|
|
||||||
|
return col, components
|
53
ui/lib/components/model.py
Normal file
53
ui/lib/components/model.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import gradio as gr
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
from .. import api, config
|
||||||
|
|
||||||
|
def create_model_column(voice_ids: Optional[list] = None) -> Tuple[gr.Column, dict]:
|
||||||
|
"""Create the model settings column."""
|
||||||
|
if voice_ids is None:
|
||||||
|
voice_ids = []
|
||||||
|
|
||||||
|
with gr.Column(scale=1) as col:
|
||||||
|
gr.Markdown("### Model Settings")
|
||||||
|
|
||||||
|
# Status button with embedded status
|
||||||
|
is_available, _ = api.check_api_status()
|
||||||
|
status_btn = gr.Button(
|
||||||
|
f"Checking TTS Service: {'Available' if is_available else 'Not Yet Available'}",
|
||||||
|
variant="secondary"
|
||||||
|
)
|
||||||
|
|
||||||
|
voice_input = gr.Dropdown(
|
||||||
|
choices=voice_ids,
|
||||||
|
label="Voice",
|
||||||
|
value=voice_ids[0] if voice_ids else None,
|
||||||
|
interactive=True
|
||||||
|
)
|
||||||
|
format_input = gr.Dropdown(
|
||||||
|
choices=config.AUDIO_FORMATS,
|
||||||
|
label="Audio Format",
|
||||||
|
value="mp3"
|
||||||
|
)
|
||||||
|
speed_input = gr.Slider(
|
||||||
|
minimum=0.5,
|
||||||
|
maximum=2.0,
|
||||||
|
value=1.0,
|
||||||
|
step=0.1,
|
||||||
|
label="Speed"
|
||||||
|
)
|
||||||
|
|
||||||
|
submit_btn = gr.Button(
|
||||||
|
"Generate Speech",
|
||||||
|
variant="primary",
|
||||||
|
size="lg"
|
||||||
|
)
|
||||||
|
|
||||||
|
components = {
|
||||||
|
"status_btn": status_btn,
|
||||||
|
"voice": voice_input,
|
||||||
|
"format": format_input,
|
||||||
|
"speed": speed_input,
|
||||||
|
"submit": submit_btn
|
||||||
|
}
|
||||||
|
|
||||||
|
return col, components
|
37
ui/lib/components/output.py
Normal file
37
ui/lib/components/output.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import gradio as gr
|
||||||
|
from typing import Tuple
|
||||||
|
from .. import files
|
||||||
|
|
||||||
|
def create_output_column() -> Tuple[gr.Column, dict]:
|
||||||
|
"""Create the output column with audio player and file list."""
|
||||||
|
with gr.Column(scale=1) as col:
|
||||||
|
gr.Markdown("### Latest Output")
|
||||||
|
audio_output = gr.Audio(
|
||||||
|
label="Generated Speech",
|
||||||
|
type="filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
gr.Markdown("### Generated Files")
|
||||||
|
output_files = gr.Dropdown(
|
||||||
|
label="Previous Outputs",
|
||||||
|
choices=files.list_output_files(),
|
||||||
|
value=None,
|
||||||
|
allow_custom_value=False
|
||||||
|
)
|
||||||
|
|
||||||
|
play_btn = gr.Button("▶️ Play Selected", size="sm")
|
||||||
|
|
||||||
|
selected_audio = gr.Audio(
|
||||||
|
label="Selected Output",
|
||||||
|
type="filepath",
|
||||||
|
visible=False
|
||||||
|
)
|
||||||
|
|
||||||
|
components = {
|
||||||
|
"audio_output": audio_output,
|
||||||
|
"output_files": output_files,
|
||||||
|
"play_btn": play_btn,
|
||||||
|
"selected_audio": selected_audio
|
||||||
|
}
|
||||||
|
|
||||||
|
return col, components
|
40
ui/lib/config.py
Normal file
40
ui/lib/config.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
API_URL = "http://kokoro-tts:8880"
|
||||||
|
|
||||||
|
# File paths
|
||||||
|
INPUTS_DIR = "/app/ui/data/inputs"
|
||||||
|
OUTPUTS_DIR = "/app/ui/data/outputs"
|
||||||
|
|
||||||
|
# Create directories if they don't exist
|
||||||
|
os.makedirs(INPUTS_DIR, exist_ok=True)
|
||||||
|
os.makedirs(OUTPUTS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Audio formats
|
||||||
|
AUDIO_FORMATS = ["mp3", "wav", "opus", "flac"]
|
||||||
|
|
||||||
|
# UI Theme
|
||||||
|
THEME = "monochrome"
|
||||||
|
CSS = """
|
||||||
|
.gradio-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-container {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-container img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
"""
|
87
ui/lib/files.py
Normal file
87
ui/lib/files.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import os
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
import datetime
|
||||||
|
from .config import INPUTS_DIR, OUTPUTS_DIR, AUDIO_FORMATS
|
||||||
|
|
||||||
|
def list_input_files() -> List[str]:
|
||||||
|
"""List all input text files."""
|
||||||
|
return [f for f in os.listdir(INPUTS_DIR) if f.endswith('.txt')]
|
||||||
|
|
||||||
|
def list_output_files() -> List[str]:
|
||||||
|
"""List all output audio files."""
|
||||||
|
return [os.path.join(OUTPUTS_DIR, f)
|
||||||
|
for f in os.listdir(OUTPUTS_DIR)
|
||||||
|
if any(f.endswith(ext) for ext in AUDIO_FORMATS)]
|
||||||
|
|
||||||
|
def read_text_file(filename: str) -> str:
|
||||||
|
"""Read content of a text file."""
|
||||||
|
if not filename:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(INPUTS_DIR, filename)
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
except:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def save_text(text: str, filename: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""Save text to a file. Returns the filename if successful."""
|
||||||
|
if not text.strip():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
# Use input_1.txt, input_2.txt, etc.
|
||||||
|
base = "input"
|
||||||
|
counter = 1
|
||||||
|
while True:
|
||||||
|
filename = f"{base}_{counter}.txt"
|
||||||
|
if not os.path.exists(os.path.join(INPUTS_DIR, filename)):
|
||||||
|
break
|
||||||
|
counter += 1
|
||||||
|
else:
|
||||||
|
# Handle duplicate filenames by adding _1, _2, etc.
|
||||||
|
base = os.path.splitext(filename)[0]
|
||||||
|
ext = os.path.splitext(filename)[1] or '.txt'
|
||||||
|
counter = 1
|
||||||
|
while os.path.exists(os.path.join(INPUTS_DIR, filename)):
|
||||||
|
filename = f"{base}_{counter}{ext}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
filepath = os.path.join(INPUTS_DIR, filename)
|
||||||
|
try:
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
return filename
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_uploaded_file(file_path: str) -> bool:
|
||||||
|
"""Save uploaded file to inputs directory. Returns True if successful."""
|
||||||
|
if not file_path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
if not filename.endswith('.txt'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create target path in inputs directory
|
||||||
|
target_path = os.path.join(INPUTS_DIR, filename)
|
||||||
|
|
||||||
|
# If file exists, add number suffix
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
counter = 1
|
||||||
|
while os.path.exists(target_path):
|
||||||
|
new_name = f"{base}_{counter}{ext}"
|
||||||
|
target_path = os.path.join(INPUTS_DIR, new_name)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# Copy file to inputs directory
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(file_path, target_path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving uploaded file: {e}")
|
||||||
|
return False
|
139
ui/lib/handlers.py
Normal file
139
ui/lib/handlers.py
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import gradio as gr
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from . import api, files
|
||||||
|
|
||||||
|
def setup_event_handlers(components: dict):
|
||||||
|
"""Set up all event handlers for the UI components."""
|
||||||
|
|
||||||
|
def refresh_status():
|
||||||
|
is_available, voices = api.check_api_status()
|
||||||
|
status = "Available" if is_available else "Unavailable"
|
||||||
|
btn_text = f"🔄 TTS Service: {status}"
|
||||||
|
|
||||||
|
if is_available and voices:
|
||||||
|
return {
|
||||||
|
components["model"]["status_btn"]: gr.update(value=btn_text),
|
||||||
|
components["model"]["voice"]: gr.update(choices=voices, value=voices[0] if voices else None)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
components["model"]["status_btn"]: gr.update(value=btn_text),
|
||||||
|
components["model"]["voice"]: gr.update(choices=[], value=None)
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_file_select(filename):
|
||||||
|
if filename:
|
||||||
|
try:
|
||||||
|
text = files.read_text_file(filename)
|
||||||
|
if text:
|
||||||
|
preview = text[:200] + "..." if len(text) > 200 else text
|
||||||
|
return gr.update(value=preview)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading file: {e}")
|
||||||
|
return gr.update(value="")
|
||||||
|
|
||||||
|
def handle_file_upload(file):
|
||||||
|
if file is None:
|
||||||
|
return gr.update(choices=files.list_input_files())
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Copy file to inputs directory
|
||||||
|
filename = os.path.basename(file.name)
|
||||||
|
target_path = os.path.join(files.INPUTS_DIR, filename)
|
||||||
|
|
||||||
|
# Handle duplicate filenames
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
counter = 1
|
||||||
|
while os.path.exists(target_path):
|
||||||
|
new_name = f"{base}_{counter}{ext}"
|
||||||
|
target_path = os.path.join(files.INPUTS_DIR, new_name)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
shutil.copy2(file.name, target_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error uploading file: {e}")
|
||||||
|
|
||||||
|
return gr.update(choices=files.list_input_files())
|
||||||
|
|
||||||
|
def generate_speech(text, selected_file, voice, format, speed):
|
||||||
|
is_available, _ = api.check_api_status()
|
||||||
|
if not is_available:
|
||||||
|
gr.Warning("TTS Service is currently unavailable")
|
||||||
|
return {
|
||||||
|
components["output"]["audio_output"]: None,
|
||||||
|
components["output"]["output_files"]: gr.update(choices=files.list_output_files())
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use text input if provided, otherwise use file content
|
||||||
|
if text and text.strip():
|
||||||
|
files.save_text(text)
|
||||||
|
final_text = text
|
||||||
|
elif selected_file:
|
||||||
|
final_text = files.read_text_file(selected_file)
|
||||||
|
else:
|
||||||
|
gr.Warning("Please enter text or select a file")
|
||||||
|
return {
|
||||||
|
components["output"]["audio_output"]: None,
|
||||||
|
components["output"]["output_files"]: gr.update(choices=files.list_output_files())
|
||||||
|
}
|
||||||
|
|
||||||
|
result = api.text_to_speech(final_text, voice, format, speed)
|
||||||
|
if result is None:
|
||||||
|
gr.Warning("Failed to generate speech. Please try again.")
|
||||||
|
return {
|
||||||
|
components["output"]["audio_output"]: None,
|
||||||
|
components["output"]["output_files"]: gr.update(choices=files.list_output_files())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
components["output"]["audio_output"]: result,
|
||||||
|
components["output"]["output_files"]: gr.update(choices=files.list_output_files(), value=os.path.basename(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
def play_selected(file_path):
|
||||||
|
if file_path and os.path.exists(file_path):
|
||||||
|
return gr.update(value=file_path, visible=True)
|
||||||
|
return gr.update(visible=False)
|
||||||
|
|
||||||
|
# Connect event handlers
|
||||||
|
components["model"]["status_btn"].click(
|
||||||
|
fn=refresh_status,
|
||||||
|
outputs=[
|
||||||
|
components["model"]["status_btn"],
|
||||||
|
components["model"]["voice"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
components["input"]["file_select"].change(
|
||||||
|
fn=handle_file_select,
|
||||||
|
inputs=[components["input"]["file_select"]],
|
||||||
|
outputs=[components["input"]["file_preview"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
components["input"]["file_upload"].upload(
|
||||||
|
fn=handle_file_upload,
|
||||||
|
inputs=[components["input"]["file_upload"]],
|
||||||
|
outputs=[components["input"]["file_select"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
components["output"]["play_btn"].click(
|
||||||
|
fn=play_selected,
|
||||||
|
inputs=[components["output"]["output_files"]],
|
||||||
|
outputs=[components["output"]["selected_audio"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
components["model"]["submit"].click(
|
||||||
|
fn=generate_speech,
|
||||||
|
inputs=[
|
||||||
|
components["input"]["text_input"],
|
||||||
|
components["input"]["file_select"],
|
||||||
|
components["model"]["voice"],
|
||||||
|
components["model"]["format"],
|
||||||
|
components["model"]["speed"]
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
components["output"]["audio_output"],
|
||||||
|
components["output"]["output_files"]
|
||||||
|
]
|
||||||
|
)
|
55
ui/lib/interface.py
Normal file
55
ui/lib/interface.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import gradio as gr
|
||||||
|
from . import api
|
||||||
|
from .components import create_input_column, create_model_column, create_output_column
|
||||||
|
from .handlers import setup_event_handlers
|
||||||
|
|
||||||
|
def create_interface():
|
||||||
|
"""Create the main Gradio interface."""
|
||||||
|
# Initial status check
|
||||||
|
is_available, available_voices = api.check_api_status()
|
||||||
|
|
||||||
|
with gr.Blocks(
|
||||||
|
title="Kokoro TTS Demo",
|
||||||
|
theme=gr.themes.Monochrome()
|
||||||
|
) as demo:
|
||||||
|
gr.HTML(value='<div style="display: flex; gap: 0;">'
|
||||||
|
'<a href="https://huggingface.co/hexgrad/Kokoro-82M" target="_blank" style="color: #2196F3; text-decoration: none; margin: 2px; border: 1px solid #2196F3; padding: 4px 8px; height: 24px; box-sizing: border-box; display: inline-flex; align-items: center;">Kokoro-82M HF Repo</a>'
|
||||||
|
'<a href="https://github.com/remsky/Kokoro-FastAPI" target="_blank" style="color: #2196F3; text-decoration: none; margin: 2px; border: 1px solid #2196F3; padding: 4px 8px; height: 24px; box-sizing: border-box; display: inline-flex; align-items: center;">Kokoro-FastAPI Repo</a>'
|
||||||
|
'</div>', show_label=False)
|
||||||
|
|
||||||
|
# Main interface
|
||||||
|
with gr.Row():
|
||||||
|
# Create columns
|
||||||
|
input_col, input_components = create_input_column()
|
||||||
|
model_col, model_components = create_model_column(available_voices) # Pass initial voices
|
||||||
|
output_col, output_components = create_output_column()
|
||||||
|
|
||||||
|
# Collect all components
|
||||||
|
components = {
|
||||||
|
"input": input_components,
|
||||||
|
"model": model_components,
|
||||||
|
"output": output_components
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up event handlers
|
||||||
|
setup_event_handlers(components)
|
||||||
|
|
||||||
|
# Add periodic status check with Timer
|
||||||
|
def update_status():
|
||||||
|
is_available, voices = api.check_api_status()
|
||||||
|
status = "Available" if is_available else "Unavailable"
|
||||||
|
return {
|
||||||
|
components["model"]["status_btn"]: gr.update(value=f"🔄 TTS Service: {status}"),
|
||||||
|
components["model"]["voice"]: gr.update(choices=voices, value=voices[0] if voices else None)
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = gr.Timer(10, active=True) # Check every 10 seconds
|
||||||
|
timer.tick(
|
||||||
|
fn=update_status,
|
||||||
|
outputs=[
|
||||||
|
components["model"]["status_btn"],
|
||||||
|
components["model"]["voice"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return demo
|
Loading…
Add table
Reference in a new issue