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/

 

Static Files - FastAPI

Static Files You can serve static files automatically from a directory using StaticFiles. Use StaticFiles Import StaticFiles. "Mount" a StaticFiles() instance in a specific path. from fastapi import FastAPI from fastapi.staticfiles import StaticFiles app =

fastapi.tiangolo.com

 

예시 코드

 

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
    },

 

TAGS.

Comments