미리 제작한 프로젝트를 변경하여 동영상으로 내보내기
제공하는 API만을 이용해서 동영상으로 내보내는 것은 현실적으로 불가능합니다. 따라서 프로젝트 에디터를 이용해 미리 프로젝트를 만들어 두고, API를 이용해 해당 프로젝트를 조회 및 변경하여 동영상으로 내보내는 것이 좋습니다.
이 가이드에서는 미리 제작한 프로젝트의 일부를 변경하여 동영상으로 내보내는 과정에 대해 설명합니다.
- 프로젝트 에디터를 이용하여 프로젝트 제작
- API를 이용하여 프로젝트 조회
- 조회된 프로젝트를 변경
- API를 이용하여 프로젝트를 동영상으로 내보내기
- API를 이용하여 완료 여부 체크
1. 프로젝트 제작하기
이번 가이드에서는 프로젝트를 미리 작성하고 이것을 수정하여 동영상을 내보내기 합니다. 프로젝트에 미리 작성할 내용은 다음과 같습니다.
- 두 개의 씬으로 이루어짐
- 각 씬의 화면에는 이미지와 텍스트 요소가 삽입되어 있음
- 각 씬에는 아바타의 대사가 입력되어 있음
또한 이 프로젝트의 내용을 변경할 때 변경해야 할 이미지, 텍스트 요소를 찾기 쉽게 하기 위해 각 요소에 태그를 붙여놓습니다.
1.1. 씬 작성
첫번째 씬 | 두번째 씬 |
---|---|
두 개의 씬으로 이루어진 프로젝트를 작성합니다. 각 씬의 화면에는 이미지와 텍스트 요소가 삽입되어 있습니다. 각 씬에는 아바타의 대사가 입력되어 있으며 특히 두번째 씬에는 두 아바타의 대화가 입력되어 있습니다.
1.2. 태그 붙이기
변경할 요소에는 찾기 쉽도록 태그를 붙여 놓습니다. 이번 가이드에서는 이미지에 parrot-image
,
텍스트에 parrot-text
태그를 붙여 놓았습니다.
2. API 키 설정
AI STUDIO V3 내 모든 API 통신 시에는 인증이 필요합니다. 이를 위해 사용되는 것이 API 키입니다. token
변수에 발급받은 API 키를 설정합니다. 아직 발급받은 키가 없다면 API 키 발급하기에서 발급하실 수 있습니다.
const token = '## API key ##'
3. 프로젝트 ID 설정
projectId
변수에 기존에 생성해둔 프로젝트 ID를 설정합니다. 프로젝트 ID는 내 스튜디오 페이지에서 기존에 저장해둔 프로젝트를 클릭하여 편집 화면으로 진입 시 URL을 통해 확인할 수 있습니다. 예를 들어 app.aistudios.com/editor/abcdefg
에서 프로젝트 ID는 abcdefg
가 됩니다.
const projectId = '## project id ##'
4. 프로젝트 가져오기 API 요청
프로젝트 가져오기 API를 이용하여 변경하여 동영상으로 내보낼 프로젝트를 조회합니다. 2. API 키 설정 단계와 3. 프로젝트 ID 설정 단계에서 설정한 API 키와 프로젝트 아이디를 이용하여 프로젝트를 조회합니다.
리퀘스트 포맷
항목 | 값 |
---|---|
Endpoint | /api/odin/v3/editor/project/<project id> |
Method | GET |
예시 코드
const API_HOST = 'https://app.aistudios.com'
const GET_PROJECT_API_PATH = '/api/odin/v3/editor/project'
const project = await fetch(
`${API_HOST}${GET_PROJECT_API_PATH}/${projectId}`,
{
method: 'GET',
headers: {
Authorization: token,
},
},
)
.then((response) => response.json())
.then((response) => {
if (response.success == true) {
return response.data.project
}
})
API 공통 팁
API 사용 권한의 인증을 위해 리퀘스트 헤더에 API 키를 입력해야 합니다. 리퀘스트 헤더의 Authorization
항목의 값으로 2. API 키 설정 단계에서 설정한 API 키를 지정해야 합니다. 대부분의 API는 사용 권한 인증이 필수이므로 API 키를 누락하지 않아야 합니다.
fetch('## endpoint ##', {
headers: {
Authorization: token,
},
})
대부분의 API의 리스폰스 바디는 동일한 포맷으로 구성되어 있습니다. 크게 성공/실패 여부와 결과 데이터로 되어 있습니다.
성공/실패 여부는 boolean
, 결과 데이터는 API 별로 다를 수 있습니다.
항목 | 타입 | 설명 |
---|---|---|
success | boolean | 결과의 성공/실패 여부 - 성공: true - 실패: false |
data | any | 결과 데이터 |
const responseBody: {
success: boolean
data: any
} = await fetch('## endpoint ##')
.then((response) => response.json())
5. 프로젝트 데이터 수정
조회된 프로젝트 데이터에는 프로젝트를 구성하는 대부분의 정보가 포함되어 있습니다. 프로젝트 데이터에 대한 대략적인 구조 설명과 몇가지 요소를 변경하는 과정을 설명하겠습니다.
프로젝트 데이터 구조
project
의 1레벨 필드들은 주로 프로젝트 전체에 영향을 주는 항목들로 구성되어 있습니다. 프로젝트의 이름, 화면의 방향, 씬 리스트 등으로 이루어져 있습니다.
project
필드
항목 | 타입 | 설명 |
---|---|---|
project.name | string | 프로젝트의 이름 |
project.orientation | 'landscape' | 'portrait' | 화면의 방향 1920x1080을 기준으로 방향에 따라 가로/세로가 바뀝니다. |
project.scenes | scene[] (json[] ) | 각 씬의 화면 구성, 아바타의 대사 |
project.scenes
에는 씬의 갯수만큼 scene
데이터가 배열 형태로 들어 있습니다. scene
은 화면의 구성 요소들과 아바타의 대사로 이루어져 있습니다. 화면의 구성 요소들은 scene.clips
에 아바타의 대사들은 scene.scripts
에 각각 배열 형태로 들어 있습니다.
화면의 구성 요소는 clip
이라는 데이터 구조를 가지고 있습니다. clip
의 종류에 따라 구성하고 있는 필드가 다릅니다. 다음은 clip
의 공통 또는 주요 필드들에 대한 설명입니다.
clip
필드
항목 | 타입 | 설명 |
---|---|---|
clip.type | 'image' | 'textImage' | ... | clip 의 종류 |
clip.left | number | clip 의 left 값(단위: px ) |
clip.top | number | clip 의 top 값(단위: px ) |
clip.width | number | clip 의 width 값(단위: px ) |
clip.height | number | clip 의 height 값(단위: px ) |
clip.scaleX | number | clip 의 가로 스케일이 값과 clip.width 를 이용해 실제 너비가 계산됨(단위: 1을 기준으로 하는 비율 값. e.g. 1.5) |
clip.scaleY | number | clip 의 세로 스케일이 값과 clip.width 를 이용해 실제 높이가 계산됨(단위: 1을 기준으로 하는 비율 값. e.g. 1.5) |
clip.tag | string | undefined | 에디터에서 사용자가 지정한 태그 |
clip
의 종류별 상세한 포맷은 Clip 속성 문서를 참고해 주세요.
아바타의 대사는 script
라는 데이터 구조를 가지고 있습니다. script
를 구성하고 있는 필드는 아바타나 보이스의 종류 등 다양한 조건에 따라 달라집니다. 이번 가이드에서는 일반적인 상황에서 입력된 대사만 수정하는 간단한 작업만 진행합니다.
단일 아바타의 일반적인 경우에는 scripts
에 script
가 하나만 존재하지만 아바타 간의 대화형인 경우 대화의 수만큼 script
가 존재합니다.
script
필드
항목 | 타입 | 설명 |
---|---|---|
script.org | string | 아바타의 대사<p /> , <span /> 태그 등을 허용하지만 plain text 만 입력하는 것을 권장 |
5.1. 이미지 교체
첫번째 씬의 이미지를 다른 이미지로 교체합니다. 1.2. 태그 붙이기 단계에서 미리 지정한 태그를 이용해 교체할 이미지를 찾고, 새로운 이미지로 교체합니다.
첫번째 씬 찾기
4. 프로젝트 가져오기 API 요청 단계에서 조회한 project
에서 첫번째 씬을 찾습니다.
const firstScene = project.scenes[0]
이미지 클립 찾기
첫번째 씬에서 교체할 이미지 클립을 찾습니다. 1.2. 태그 붙이기 단계에서 parrot-image
라는 태그를 붙여두었습니다. 태그로 parrot-image
가 입력된 클립을 찾습니다.
const imageTag = 'parrot-image'
const imageClip = firstScene.clips.find((clip) => clip.tag === imageTag)
이미지 교체하기
이미지 클립은 공통 필드 이외에 이미지 용의 추가 필드를 갖습니다.
이미지 clip
추가 필드
항목 | 타입 | 설명 |
---|---|---|
source_url | string | 이미지 주소 (URL) |
해당 필드에 교체할 이미지의 주소를 입력합니다.
const replacingImageUrl = '## replacing image url ##'
imageClip.resource_url = replacingImageUrl
이전 이미지의 크기와 새로운 이미지의 크기가 같다면 이미지 주소만 교체하면 됩니다. 하지만 크기가 다르다면 위치, 크기 등의 속성을 알맞게 지정하는 것도 고려해야 합니다.
imageClip.left = 15
imageClip.width = 15
imageClip.scaleX = 1.5
교체할 이미지는 공개된 위치에 존재해야 하고 외부에서의 접근이 가능해야 합니다. 동영상 내보내기 중 이미지에 접근하지 못 하는 경우에는 오류가 발생하거나 결과 동영상에 이미지가 표시되지 않을 수 있습니다.
- 공개 URL을 가지는지 확인
- 외부 네트워크에서 접근이 가능한지 확인
- 접근 권한 등에 의해 차단되지 않는지 확인
5.2. 텍스트 교체
첫번째 씬의 텍스트를 다른 텍스트로 교체합니다.
텍스트 클립 찾기
첫번째 씬에서 교체할 텍스트 클립을 찾습니다. parrot-text
가 입력된 클립을 찾습니다.
const textTag = 'parrot-text'
const textClip = firstScene.clips.find((clip) => clip.tag === textTag)
텍스트 교체하기
텍스트 클립은 공통 필드 외에 텍스트 용의 추가 필드를 갖습니다.
텍스트 clip
추가 필드
항목 | 타입 | 설명 |
---|---|---|
text | string | 입력된 텍스트 |
fontSize | number | 폰트 크기 (단위: pt ) |
해당 필드에 교체할 텍스트를 입력합니다.
const replacingTextSentence = '## replacing text sentence ##'
textClip.text = replacingTextSentence
교체할 텍스트의 길이가 다르다면 위치, 크기 등의 속성을 적절하게 다시 지정하는 것도 고려해야 합니다.
또한 fontSize
도 변경할 수 있습니다.
fontSize
의 단위는 pt
입니다.
하지만 프로젝트의 공통 단위는 px
로 주의가 필요합니다.
pt
와 px
는 3 : 4 비율입니다.
const changingFontSizePx = 10
const changingFontSizePt = changingFontSizePx * 3 / 4
textClip.fontSize = changingFontSizePt
5.3. 스크립트 교체
첫번째 씬의 대사와 두번째 씬의 두번째 대사를 변경합니다. 대사는 각 씬에 배열로 입력되어 있습니다. 아바타가 하나인 내레이션 유형의 대사인 경우에도 대사는 배열로 입력되어 있으며 이 때는 배열의 아이템이 하나만 존재합니다. 아바타가 둘인 대화 유형의 대사인 경우에는 대화의 수만큼 존재합니다.
첫번째 씬 대사 교체
const replacingFirstSceneScript = '## replacing first scene script ##'
const firstSceneScript = firstScene.scripts[0]
firstSceneScript.org = replacingFirstSceneScript
두번째 씬의 두번째 대사 교체
const secondScene = project.scenes[1]
const replacingSecondSceneScript = '## replacing second scene script ##'
const secondSceneScript = secondScene.scripts[1]
// ^ second script
secondSceneScript.org = replacingSecondSceneScript
6. 프로젝트 내보내기 API 요청
수정한 씬 데이터를 프로젝트 내보내기 API에 요청합니다. '내보내기'란 영상을 생성하기 위한 합성 요청을 의미하며, 프로젝트 내보내기 API 요청 시 보낼 수 있는 전체 데이터 종류는 여기에서 자세히 확인하실 수 있습니다.
리퀘스트 포맷
항목 | 값 |
---|---|
Endpoint | /api/odin/v3/editor/project |
Method | POST |
Body | Partial<project> (json ) |
예시 코드
const GENERATE_PROJECT_API_PATH = '/api/odin/v3/editor/project'
const stringifiedProject = JSON.stringify({
name: project.name,
orientation: project.orientation,
scenes: project.scenes,
})
const generatedProjectId = await fetch(
`${API_HOST}${GENERATE_PROJECT_API_PATH}`
{
method: 'POST',
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
body: stringifiedProject,
},
)
.then((response) => response.json())
.then((response) => {
if (response.success === true) {
return response.data.projectId
}
})
7. 프로젝트 진행률 확인 및 완료
동영상을 내보내는 과정에는 시간이 소요됩니다. 6. 프로젝트 내보내기 API 요청 단계는 내보내기를 시작하는 기능입니다. 내보내기 과정의 진행도를 확인하여 완료 여부를 판단해야 합니다.
이번 가이드에서는 1분에 한 번 씩 진행도를 확인하여 완료 여부를 확인합니다.
리퀘스트 포맷
항목 | 값 |
---|---|
Endpoint | /api/odin/v3/editor/progress/<project id> |
Method | GET |
예시 코드
const GET_PROGRESS_API_PATH = '/api/odin/v3/editor/progress'
const delay = async (ms = 1000 * 60) => {
await new Promise(r => setTimeout(r, ms))
}
let isFinished = false
let videoUrl = ''
while (!isFinished) {
const progressData = await fetch(
`${API_HOST}${GET_PROGRESS_API_PATH}/${generatedProjectId}`,
{
method: 'GET',
headers: {
Authorization: token,
},
},
)
.then((response) => response.json())
.then((response) => {
if (response.success === true) {
return response.data
}
})
if (progressData.progress < 100) {
await delay()
}
else {
videoUrl = progressData.downloadUrl
isFinished = true
}
}
전체 코드
const API_HOST = 'https://app.aistudios.com'
const GET_PROJECT_API_PATH = '/api/odin/v3/editor/project'
const GENERATE_PROJECT_API_PATH = '/api/odin/v3/editor/project'
const GET_PROGRESS_API_PATH = '/api/odin/v3/editor/progress'
const token = '## API key ##'
const projectId = '## project id ##'
const replacingImageUrl = '## replacing image url ##'
const replacingTextSentence = '## replacing text sentence ##'
const replacingFirstSceneScript = '## replacing first scene script ##'
const replacingSecondSceneScript = '## replacing second scene script ##'
const delay = async (ms = 1000 * 60) => {
await new Promise(r => setTimeout(r, ms))
}
const main = async () => {
const project = await fetch(
`${API_HOST}${GET_PROJECT_API_PATH}/${projectId}`,
{
method: 'GET',
headers: {
Authorization: token,
},
},
)
.then((response) => response.json())
.then((response) => {
if (response.success == true) {
return response.data.project
}
})
const imageTag = 'parrot-image'
const imageClip = firstScene.clips.find((clip) => clip.tag === imageTag)
imageClip.resource_url = replacingImageUrl
const textTag = 'parrot-text'
const textClip = firstScene.clips.find((clip) => clip.tag === textTag)
textClip.text = replacingTextSentence
const firstSceneScript = firstScene.scripts[0]
firstSceneScript.org = replacingFirstSceneScript
const secondScene = project.scenes[1]
const secondSceneScript = secondScene.scripts[1]
secondSceneScript.org = replacingSecondSceneScript
const stringifiedProject = JSON.stringify({
name: project.name,
orientation: project.orientation,
scenes: project.scenes,
})
const generatedProjectId = await fetch(
`${API_HOST}${GENERATE_PROJECT_API_PATH}`,
{
method: 'POST',
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
body: stringifiedProject,
},
)
.then((response) => response.json())
.then((response) => {
if (response.success === true) {
return response.data.projectId
}
})
let isFinished = false
let videoUrl = ''
while (!isFinished) {
const progressData = await fetch(
`${API_HOST}${GET_PROGRESS_API_PATH}/${generatedProjectId}`,
{
method: 'GET',
headers: {
Authorization: token,
},
},
)
.then((response) => response.json())
.then((response) => {
if (response.success === true) {
return response.data
}
})
if (progressData.progress < 100) {
await delay()
}
else {
videoUrl = progressData.downloadUrl
isFinished = true
}
}
console.log(videoUrl)
}
main()