I am creating a web application that receives some text, converts the text into speech, and returns an mp3 file, which is saved to a temporary directory.
I want to be able to download the file from the html page (i.e., the frontend), but I don't know how to do that properly.
I know with Flask you can do this:
from app import app
from flask import Flask, send_file, render_template
@app.route('/')
def upload_form():
return render_template('download.html')
@app.route('/download')
def download_file():
path = "html2pdf.pdf"
return send_file(path, as_attachment=True)
if __name__ == "__main__":
app.run()
HTML Example:
<!doctype html>
<title>Python Flask File Download Example</title>
<h2>Download a file</h2>
<p><a href="{{ url_for('.download_file') }}">Download</a></p>
So how do I replicate this with FastAPI?
FastAPI Code:
from fastapi import FastAPI, File, Request, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from gtts import gTTS
templates = Jinja2Templates(directory="templates")
def text_to_speech(language:str, text: str) -> str:
tts = gTTS(text=text, lang=language, slow=False)
tts.save("./temp/welcome.mp3")
#os.system("mpg321 /temp/welcome.mp3")
return "Text to speech conversion successful"
@app.get("/")
def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/text2speech")
async def home(request: Request):
if request.method == "POST":
form = await request.form()
if form["message"] and form["language"]:
language = form["language"]
text = form["message"]
translate = text_to_speech(language, text)
path = './temp/welcome.mp3'
value = FileResponse("./temp/welcome.mp3", media_type="audio/mp3")
return value
# return templates.TemplateResponse(
# "index.html",
# {"request": request, "message": text, "language": language, "download": value},
# )
Sample HTML File:
<!doctype html>
<title>Download MP3 File</title>
<h2>Download a file</h2>
<p><a href="{{ url_for('text2speech') }}">Download</a></p>
Use the Form
keyword to define Form-data
in your endpoint, and more specifically, use Form(...)
to make a parameter required, instead of using await request.form()
and manually checking if the user submitted the required parameters. After processing the received data and generating the audio file, you can use FileResponse
to return the file to the user. Note: use the headers
argument in FileResponse
to set the Content-Disposition
header using the attachment
parameter—as described in this answer—to have the file downloaded to your device. Failing to set the headers
, or using the inline
parameter isntead, would lead to 405 Method Not Allowed
error, as, in that case, the browser would attempt accessing the file using a GET
request, in order to display the file contents inline (however, only POST
requests are allowed to your /text2speech
endpoint). Have a look at Option 1 in the examples below.
If you wanted the /text2speech
endpoint supporting both GET
and POST
requests (as shown in your question), you could either (1) use @app.api_route("/text2speech", methods=["GET", "POST"])
and use request.method
to check whether it is a GET
or POST
request (see this answer for a working example), or (2) define two different endpoints with the following decorators on each one, i.e., @app.post('/text2speech')
and @app.get('/text2speech')
(Side note: These decorators could also be used on the same endpoint/function, and then use request.method
as described in (1) option). However, you don't necessarily need to do that in this case.
Additionally, you have added a Download
hyperlink to your template for the user to download the file. However, you haven't provided any information as to how you expect this to work. This wouldn't work in a scenario where you don't have static
files, but dynamically generated audio files (as in your case), as well as multiple users accessing the API at the same time; unless, for example, you generated random UUIDs for the filenames and saved the files to a StaticFiles
directory—or added that unique identifier as a query/path parameter (you could also use cookies instead, see here and here) to the URL in order to identify the file to be downloaded—and sent the URL back to the user. In that case, you would need a Javascript interface/library, such as Fetch API, to make an asynchronous HTTP
request—as described in this answer—in order to get the URL to the file and display it in the Download
hyperlink. Have a look at Option 2 below. Note: The example in Option 2, for demo purposes, uses a simple dict
to map the filepaths to UUIDs. In a real-world scenario, however, where multiple users access the API and several workers might be used, you may consider using a database storage, or Key-Value stores (Caches), as described here and here. You would also need to have a mechanism for deleting the files from the database and disk, once they have been downloaded, as well as make sure that users do not have unauthorized access to other users' audio files. It is also worth mentioning that in the same example, the UUID is expected as a query parameter to the /download
endpoint, but in a real-world scenario, you should never pass sensitive information to the query string, as this would pose security/privacy risks (more details on that are given in Solution 1 of this answer). Instead, you should pass sensitive data to the request body (similar to Solution 2 of this answer), and always use the HTTPS protocol.
app.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import os
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get('/')
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
# do some processing here
filepath = './temp/welcome.mp3'
filename = os.path.basename(filepath)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return FileResponse(filepath, headers=headers, media_type="audio/mp3")
An alternative to the above would be to read the file data inside your endpoint—or, in case the data were fully loaded into memory beforehand, such as here, here and here—and return a custom Response
directly, as shown below:
from fastapi import Response
@app.post('/text2speech')
...
with open(filepath, "rb") as f:
contents = f.read() # file contents could be already fully loaded into RAM
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return Response(contents, headers=headers, media_type='audio/mp3')
In case you had to return a file that is too large to fit into memory—e.g., if you have 8GB of RAM, you can’t load a 50GB file—you could use StreamingResponse
, which would load the file into memory in chunks and process the data one chunk at a time. If you find yield from f
, shown in the example below, being rather slow, please have a look at this answer for faster alternatives. It should also be noted that using FileResponse
would also load the file contents into memory in chunks (instead of the entire contents at once); however, the chunk size in that case would be 64KB, as specified in the implementation class of FileResponse
. Thus, if that chunk size does not suit your requirements, you could instead use a StreamingResponse
, as demonstrated below, or as shown in this this answer, by specifying the chunk size as desired.
from fastapi.responses import StreamingResponse
@app.post('/text2speech')
...
def iterfile():
with open(filepath, "rb") as f:
yield from f
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return StreamingResponse(iterfile(), headers=headers, media_type="audio/mp3")
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Convert Text to Speech</title>
</head>
<body>
<form method="post" action="http://127.0.0.1:8000/text2speech">
message : <input type="text" name="message" value="This is a sample message"><br>
language : <input type="text" name="language" value="en"><br>
<input type="submit" value="submit">
</form>
</body>
</html>
In case you used a JavaScript interface, such as Fetch API, in the frontend to issue the file-download request—instead of using an HTML <form>
, as demonstrated above—please have a look at this answer, as well as this answer and this answer on how to download the file in the frontend through JavaScript.
app.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import uuid
import os
app = FastAPI()
templates = Jinja2Templates(directory="templates")
files = {}
@app.get('/')
async def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get('/download')
def download_file(request: Request, fileId: str):
filepath = files.get(fileId)
if filepath:
filename = os.path.basename(filepath)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return FileResponse(filepath, headers=headers, media_type='audio/mp3')
@app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
# do some processing here
filepath = './temp/welcome.mp3'
file_id = str(uuid.uuid4())
files[file_id] = filepath
file_url = f'/download?fileId={file_id}'
return {"fileURL": file_url}
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Convert Text to Speech</title>
</head>
<body>
<form method="post" id="myForm">
message : <input type="text" name="message" value="This is a sample message"><br>
language : <input type="text" name="language" value="en"><br>
<input type="button" value="Submit" onclick="submitForm()">
</form>
<a id="downloadLink" href=""></a>
<script type="text/javascript">
function submitForm() {
var formElement = document.getElementById('myForm');
var data = new FormData(formElement);
fetch('/text2speech', {
method: 'POST',
body: data,
})
.then(response => response.json())
.then(data => {
document.getElementById("downloadLink").href = data.fileURL;
document.getElementById("downloadLink").innerHTML = "Download";
})
.catch(error => {
console.error(error);
});
}
</script>
</body>
</html>
For Option 1 above, to remove a file after it has been downloaded by the user, you can simply define a BackgroundTask
to be run after returning the response. For example:
from fastapi import BackgroundTasks
import os
@app.post('/text2speech')
def convert(request: Request, background_tasks: BackgroundTasks, ...):
filepath = 'welcome.mp3'
# ...
background_tasks.add_task(os.remove, path=filepath)
return FileResponse(filepath, headers=headers, media_type="audio/mp3")
For Option 2, however, you would have to make sure to delete the key (i.e., file_id
) pointing to the given filepath from the cache as well. Hence, you should create a background task function, as shown below:
from fastapi import BackgroundTasks
import os
files = {}
def remove_file(filepath, fileId):
os.remove(filepath)
del files[fileId]
@app.get('/download')
def download_file(request: Request, fileId: str, background_tasks: BackgroundTasks):
filepath = files.get(fileId)
if filepath:
# ...
background_tasks.add_task(remove_file, filepath=filepath, fileId=fileId)
return FileResponse(filepath, headers=headers, media_type='audio/mp3')
More details and examples on background tasks can be found here, as well as here.