FastAPI에서 front로 파일을 제공하는 방법 - static file serving, Fileresponse + vue.js에서 음성파일 재생하기
1. static file serving
FastAPI에서 만든 정적 파일(static file, HTML, CSS, Javascript, 이미지, 음성파일 등)을 front에 제공하고 싶을때, 한가지 방법
정적 파일 경로를 지정하고, frontend에서 해당 경로로 직접 접근하여 파일을 사용하는 방법
공식 문서 피셜
https://fastapi.tiangolo.com/tutorial/static-files/
예시 코드
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
앞에 경로 /static은 frontend에서 접근을 위해 사용하기 위한 경로 지정
directory = "static"은 실제 서버 내 static 파일이 저장되는 폴더
name = "static"은 Fastapi에서 내부적으로 인식하기 위해 사용되는 이름
예시를 위해 모두 static이라고 사용했지만 그렇지 않아도 된다
저렇게 설정했을때, 실제 사용하고 싶다면..?
로컬에서 사용할때는 http://localhost:8000/static/(파일이름)으로 접근하면 된다
음성파일을 모아놓은 정적 폴더구조가 /static/sound/ 이런다면..
"http://localhost:8000/static/sound/(파일이름)"으로 접근하면 오디오 재생이 가능하다
옵션에 대해서 좀 분석하고 넘어가면
StaticFiles(directory="")부분은 실제 존재하는 폴더명을 정확히 입력해야함
static이라고 있는데 stat이라고 쓴다면
그리고 폴더가 static이라고 있는데.. 앞에 경로를 /stat이라고 하고 하면
http://localhost:8000/stat/sound/(파일명)이라고 접근한다면.. 접근 가능하다
근데 /stat이라고 지정했는데, http://localhost:8000/static/sound/(파일명)이라고 접근하면.. 접근을 못함
<template>
<div>
<input type="text" v-model="inputValue" />
<button @click="sendInputValue">전송</button>
<div>{{storyResult}}</div>
<button @click="playAudio(sound)">오디오 재생</button>
<!-- <StoryChatGptView :storyResult="storyResult"></StoryChatGptView> -->
</div>
</template>
<script>
import axios from 'axios'
import StoryChatGptView from './StoryChatGptView.vue'
export default {
components: {
StoryChatGptView
},
data () {
return {
inputValue: '',
storyResult: '',
rescode: '',
sound: 'http://localhost:8000/static/sound/'
}
},
methods: {
playAudio (sound) {
if (sound) {
const audio = new Audio(sound)
audio.play()
}
},
sendInputValue () {
console.log(typeof this.inputValue)
axios
.post(`/fast/stories/gpt`, {
text: this.inputValue
})
.then(result => {
this.storyResult = result.data.story
axios({
url: `/fast/stories/sound`,
method: 'post',
data: {
data: this.storyResult
}
})
.then(result => {
console.log(result)
this.rescode = result.data.rescode
this.sound += result.data.sound
console.log(this.sound)
})
.catch(err => {
console.log(err)
})
})
.catch(err => {
console.log(err)
})
}
}
}
</script>
vue.js에서 오디오 재생은 참고로.. const audio = new Audio((파일경로))로 오디오 객체 만들고
audio.play()하면 오디오 플레이가 된다
2. Fileresponse
다른 사람이 만들어놓은 코드인데..
혹시 언젠가 쓸지도 몰라
파일을 /static/sound/(파일명)으로 만들어놨으면..
path, filename, media_type을 다음과 같이 지정하면 파일이 return되는듯
지금 내 경우 음성파일을 return함
# File download
@app.get("/fast/file/download/{filename}")
def download_file(filename: str):
return FileResponse(path=f"static/sound/{filename}", filename=f"{filename}", media_type="multipart/form-data")
response type을 blob이라고 지정하고
this.voiceBlob = new Blob([response.data], {type: 'audio/mp3'})으로 하면 blob파일로 받나보네
<script>
import axios from 'axios'
export default {
name: 'StoryPaintingBoardView',
data () {
return {
keyword: '',
storyResult: '',
sound_filename: '',
storyReady: false,
canvas: Object,
ctx: Object,
isPainting: false,
mode: 'brush',
colorOptions1: ['#ff0000', '#ff8c00', '#ffff00', '#008000'],
colorOptions2: ['#0000ff', '#800080', '#000080', '#000000'],
title: '',
color: '',
voiceBlob: {}
}
},
created () {
this.keyword = this.$route.params.inputValue
this.generateStory()
},
mounted () {
this.canvas = this.$refs.canvas
this.ctx = this.canvas.getContext('2d')
this.ctx.lineWidth = 10
this.ctx.lineJoin = 'round'
this.ctx.lineCap = 'round'
this.onResetClick()
},
methods: {
generateStory () {
axios
.post(`/fast/stories/gpt`, {
text: this.keyword
})
.then(result => {
console.log(this.storyResult)
this.storyResult = result.data.story
axios({
url: `/fast/stories/sound`,
method: 'post',
data: {
data: this.storyResult
}
})
.then(result => {
console.log(result)
this.sound_filename = result.data.sound
this.storyReady = true
###############################################################################
axios({
url: `/fast/file/download/${this.sound_filename}`,
method: 'GET',
responseType: 'blob'
}).then((response) => {
this.voiceBlob = new Blob([response.data], {type: 'audio/mp3'})
console.log(this.voiceBlob)
})
################################################################################
})
.catch(err => {
alert('TTS 생성 중 문제 발생' + err)
})
})
.catch(err => {
alert('동화 생성 중 문제 발생' + err)
})
},
blob 파일을 window.URL.createObjectURL에 넣으면.. URL에 접근할 수 있게 만들어주는듯
var blobURL = window.URL.createObjectURL(this.voiceBlob)
this.audio = new Audio(blobURL)
그 url을 new Audio에 넣어서 새로운 Audio 객체를 만들면 fastapi에서 받은 오디오 파일을 실행할 수 있다
그리고 blob을 file 객체로 만드는 방법이 있다
new File([voiceBlob], 'story_voice_' + milliseconds + '.wav', { type: 'audio/wav' })
import { mapActions } from 'vuex'
const storyStore = 'storyStore'
export default {
name: 'StoryResultView',
data () {
return {
title: '',
painting: '',
story: null,
voiceBlob: {},
audio: Object,
stop: true
}
},
created () {
this.painting = this.$route.params.painting
this.story = this.$route.params.story
this.voiceBlob = this.$route.params.voiceBlob
},
mounted () {
this.canvas = this.$refs.canvas
this.ctx = this.canvas.getContext('2d')
let image = new Image()
image.src = this.painting
image.onload = function () {
this.ctx.drawImage(image, 0, 0, 500, 500)
}.bind(this)
this.createAudio()
},
methods: {
...mapActions(storyStore, ['saveStory']),
##fastapi에서 받은 파일을 URL로 만들어 new Audio가 접근하도록
#######################################################################
createAudio () {
var blobURL = window.URL.createObjectURL(this.voiceBlob)
this.audio = new Audio(blobURL)
},
#######################################################################
playAudio (stop) {
if (stop) {
this.audio.play()
}
this.stop = !stop
},
stopAudio (stop) {
if (stop === false) {
this.audio.pause()
}
this.stop = !stop
},
restartAudio () {
this.audio.currentTime = 0
this.audio.play()
},
canvasToFile (canvas, milliseconds) {
// canvas -> dataURL
let imgBase64 = canvas.toDataURL('image/png')
const byteString = atob(imgBase64.split(',')[1])
const ab = new ArrayBuffer(byteString.length)
const ia = new Uint8Array(ab)
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
const blob = new Blob([ab], { type: 'image/png' })
// blob -> file
const paintingFile = new File([blob], 'story_painting_' + milliseconds + '.png', { type: 'image/png' })
return paintingFile
},
########################### blob을 파일객체로 만든다
voiceBlobToFile (voiceBlob, milliseconds) {
return new File([voiceBlob], 'story_voice_' + milliseconds + '.wav', { type: 'audio/wav' })
},
onSaveClick () { // 임시 저장. (api 확인 XXXX)
if (this.title.trim() === '') {
alert('제목을 입력해주세요.')
return
}
let milliseconds = new Date().getMilliseconds()
const paintingFile = this.canvasToFile(this.canvas, milliseconds)
const voiceFile = this.voiceBlobToFile(this.voiceBlob, milliseconds)
let data = {
title: this.title,
content: this.story
}
let formData = new FormData()
formData.append('voiceFile', voiceFile)
formData.append('imageFile', paintingFile)
formData.append('data', new Blob([JSON.stringify(data)], {type: 'application/json'}))
this.saveStory(formData)
.then(
alert('동화 저장에 성공했습니다.')
)
.catch(error => {
alert('동화 저장에 실패했습니다.' + error)
})
this.audio.pause()
this.$router.push('/story/list')
}
}
}
</script>
3. 음성파일 재생
const audio = new Audio((파일명))으로 Audio 객체를 생성하면..
audio.play()하면 audio를 재생하고
audio.pause()하면 재생하던 audio를 멈춘다
audio.currentTime은 오디오의 현재 재생 시점을 말한다..
audio.play()하면서 이게 0부터 audio의 끝까지 기록이 되고 있다..
그러다가 중간에 audio.pause()하고 다시 audio.play()하면 놀랍게도 멈춘지점부터 다시 시작해준다..
audio.currentTime = 0으로 하면 강제로 처음부터 다시 시작하도록
아래는 시작버튼 누르면.. 시작만 되고 멈춤버튼 누르면 멈춤만 되도록
this.stop = !stop이 if문 안으로 들어가야할것 같은데 일단 냅두자
<template>
<div>
<label class="title">제목 : </label>
<input type="text" v-model="title">
<button @click="playAudio(stop)" class="ae-btn btn-red">오디오재생</button>
<button @click="stopAudio(stop)" class="ae-btn btn-red">오디오멈춤</button>
<button @click="restartAudio()" class="ae-btn btn-red"> 오디오 처음부터 듣기 </button>
<button @click="onSaveClick" class="ae-btn btn-red">동화 저장</button>
<div class="container">
<div class="container-left">
<canvas id="canvas"
ref="canvas"
width="500"
height="500"
></canvas>
</div>
<div class="container-right">
{{ story }}
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
const storyStore = 'storyStore'
export default {
name: 'StoryResultView',
data () {
return {
title: '',
painting: '',
story: null,
voiceBlob: {},
audio: Object,
stop: true
}
},
created () {
this.painting = this.$route.params.painting
this.story = this.$route.params.story
this.voiceBlob = this.$route.params.voiceBlob
},
mounted () {
this.canvas = this.$refs.canvas
this.ctx = this.canvas.getContext('2d')
let image = new Image()
image.src = this.painting
image.onload = function () {
this.ctx.drawImage(image, 0, 0, 500, 500)
}.bind(this)
this.createAudio()
},
methods: {
...mapActions(storyStore, ['saveStory']),
createAudio () {
var blobURL = window.URL.createObjectURL(this.voiceBlob)
this.audio = new Audio(blobURL)
},
playAudio (stop) {
if (stop) {
this.audio.play()
this.stop = !stop
}
},
stopAudio (stop) {
if (stop === false) {
this.audio.pause()
this.stop = !stop
}
},
restartAudio () {
this.audio.pause()
this.audio.currentTime = 0
this.audio.play()
this.stop = !stop
},
'프로그래밍 > FastAPI' 카테고리의 다른 글
(Python) 분명히 패키지를 설치하고 FastAPI를 실행했는데 패키지를 찾지 못하는 에러 대처하기(ModuleNotFoundError) (0) | 2024.03.06 |
---|---|
frontend(vue.js)에서 FastAPI로 타입이 여러개 담긴 Formdata 보내기 (0) | 2023.05.14 |
front에서 데이터를 제대로 보냈는데 fastapi에서 422 unprocessable entity 에러 나는 경우 (0) | 2023.05.03 |
중요한 변수를 숨겨야할 때, 사용할 수 있는 환경변수(env)설정 (0) | 2023.04.17 |
python 프로그램 수행을 위한 FastAPI 백엔드 서버 구축하기 (0) | 2023.04.17 |