creatomate 가사 정합
parent
94be8a0746
commit
198513f237
|
|
@ -24,8 +24,7 @@ from app.dependencies.pagination import (
|
|||
)
|
||||
from app.home.models import Project
|
||||
from app.lyric.models import Lyric
|
||||
from app.song.models import Song
|
||||
from app.song.models import SongTimestamp
|
||||
from app.song.models import Song, SongTimestamp
|
||||
|
||||
from app.song.schemas.song_schema import (
|
||||
DownloadSongResponse,
|
||||
|
|
@ -335,7 +334,7 @@ GET /song/status/abc123...
|
|||
},
|
||||
)
|
||||
async def get_song_status(
|
||||
suno_task_id: str,
|
||||
song_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> PollingSongResponse:
|
||||
|
|
@ -345,6 +344,7 @@ async def get_song_status(
|
|||
Azure Blob Storage에 업로드한 뒤 Song 테이블의 status를 completed로,
|
||||
song_result_url을 Blob URL로 업데이트합니다.
|
||||
"""
|
||||
suno_task_id = song_id # 임시방편 / 외부 suno 노출 방지
|
||||
logger.info(f"[get_song_status] START - song_id: {suno_task_id}")
|
||||
try:
|
||||
suno_service = SunoService()
|
||||
|
|
@ -377,44 +377,6 @@ async def get_song_status(
|
|||
)
|
||||
song = song_result.scalar_one_or_none()
|
||||
|
||||
suno_audio_id = first_clip.id
|
||||
word_data = await suno_service.get_lyric_timestamp(suno_task_id, suno_audio_id)
|
||||
lyric_result = await session.execute(
|
||||
select(Lyric)
|
||||
.where(Lyric.task_id == song.task_id)
|
||||
.order_by(Lyric.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
lyric = lyric_result.scalar_one_or_none()
|
||||
gt_lyric = lyric.lyric_result
|
||||
lyric_line_list = gt_lyric.split("\n")
|
||||
sentences = [lyric_line.strip(',. ') for lyric_line in lyric_line_list if lyric_line and lyric_line != "---"]
|
||||
|
||||
timestamped_lyrics = await suno_service.align_lyrics(word_data, sentences)
|
||||
# TODO : DB upload timestamped_lyrics
|
||||
for order_idx, timestamped_lyric in enumerate(timestamped_lyrics):
|
||||
song_timestamp = SongTimestamp(
|
||||
suno_audio_id = suno_audio_id,
|
||||
order_idx = order_idx,
|
||||
lyric_line = timestamped_lyric["text"],
|
||||
start_time = timestamped_lyric["start_sec"],
|
||||
end_time = timestamped_lyric["end_sec"]
|
||||
)
|
||||
session.add(song_timestamp)
|
||||
|
||||
await session.commit()
|
||||
|
||||
if song and song.status != "completed":
|
||||
# 첫 번째 클립의 audio_url과 duration을 직접 DB에 저장
|
||||
song.status = "completed"
|
||||
song.song_result_url = audio_url
|
||||
if clip_duration is not None:
|
||||
song.duration = clip_duration
|
||||
await session.commit()
|
||||
logger.info(f"[get_song_status] Song updated - suno_task_id: {suno_task_id}, status: completed, song_result_url: {audio_url}, duration: {clip_duration}")
|
||||
elif song and song.status == "completed":
|
||||
logger.info(f"[get_song_status] SKIPPED - Song already completed, suno_task_id: {suno_task_id}")
|
||||
|
||||
# processing 상태인 경우에만 백그라운드 태스크 실행 (중복 방지)
|
||||
if song and song.status == "processing":
|
||||
# store_name 조회
|
||||
|
|
@ -440,6 +402,33 @@ async def get_song_status(
|
|||
)
|
||||
logger.info(f"[get_song_status] Background task scheduled - song_id: {suno_task_id}, store_name: {store_name}")
|
||||
|
||||
suno_audio_id = first_clip.get('id')
|
||||
word_data = await suno_service.get_lyric_timestamp(suno_task_id, suno_audio_id)
|
||||
lyric_result = await session.execute(
|
||||
select(Lyric)
|
||||
.where(Lyric.task_id == song.task_id)
|
||||
.order_by(Lyric.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
lyric = lyric_result.scalar_one_or_none()
|
||||
gt_lyric = lyric.lyric_result
|
||||
lyric_line_list = gt_lyric.split("\n")
|
||||
sentences = [lyric_line.strip(',. ') for lyric_line in lyric_line_list if lyric_line and lyric_line != "---"]
|
||||
|
||||
timestamped_lyrics = suno_service.align_lyrics(word_data, sentences)
|
||||
# TODO : DB upload timestamped_lyrics
|
||||
for order_idx, timestamped_lyric in enumerate(timestamped_lyrics):
|
||||
song_timestamp = SongTimestamp(
|
||||
suno_audio_id = suno_audio_id,
|
||||
order_idx = order_idx,
|
||||
lyric_line = timestamped_lyric["text"],
|
||||
start_time = timestamped_lyric["start_sec"],
|
||||
end_time = timestamped_lyric["end_sec"]
|
||||
)
|
||||
session.add(song_timestamp)
|
||||
|
||||
await session.commit()
|
||||
|
||||
elif song and song.status == "uploading":
|
||||
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {suno_task_id}")
|
||||
elif song and song.status == "completed":
|
||||
|
|
|
|||
|
|
@ -58,6 +58,77 @@ CACHE_TTL_SECONDS = 300
|
|||
# 모듈 레벨 공유 HTTP 클라이언트 (커넥션 풀 재사용)
|
||||
_shared_client: httpx.AsyncClient | None = None
|
||||
|
||||
text_template_v_1 = {
|
||||
"type": "composition",
|
||||
"track": 3,
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"time": 0,
|
||||
"y": "80%",
|
||||
"width": "100%",
|
||||
"height": "40%",
|
||||
"x_alignment": "50%",
|
||||
"y_alignment": "50%",
|
||||
"text": "Lorem ipsum dolor sit amet, consectetur ad",
|
||||
"font_family": "Noto Sans",
|
||||
"font_weight": "700",
|
||||
"font_size": "8 vmin",
|
||||
"background_color": "rgba(216,216,216,0)",
|
||||
"background_x_padding": "33%",
|
||||
"background_y_padding": "7%",
|
||||
"background_border_radius": "28%",
|
||||
"fill_color": "#ffffff",
|
||||
"stroke_color": "#333333",
|
||||
"stroke_width": "1.25 vmin",
|
||||
}
|
||||
]
|
||||
}
|
||||
text_template_v_2 = {
|
||||
"type": "composition",
|
||||
"track": 3,
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"time": 0,
|
||||
"x": "7.7233%",
|
||||
"y": "89.5758%",
|
||||
"width": "84.5534%",
|
||||
"height": "5.7015%",
|
||||
"x_anchor": "0%",
|
||||
"y_anchor": "0%",
|
||||
"y_alignment": "100%",
|
||||
"font_family": "Noto Sans",
|
||||
"font_weight": "700",
|
||||
"font_size": "6.9999 vmin",
|
||||
"fill_color": "#ffffff"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
text_template_h_1 = {
|
||||
"type": "composition",
|
||||
"track": 3,
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"time": 0,
|
||||
"x": "10%",
|
||||
"y": "80%",
|
||||
"width": "80%",
|
||||
"height": "15%",
|
||||
"x_anchor": "0%",
|
||||
"y_anchor": "0%",
|
||||
"x_alignment": "50%",
|
||||
"font_family": "Noto Sans",
|
||||
"font_weight": "700",
|
||||
"font_size": "5.9998 vmin",
|
||||
"fill_color": "#ffffff",
|
||||
"stroke_color": "#333333",
|
||||
"stroke_width": "0.2 vmin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def get_shared_client() -> httpx.AsyncClient:
|
||||
"""공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다."""
|
||||
|
|
@ -470,3 +541,22 @@ class CreatomateService:
|
|||
)
|
||||
|
||||
return new_template
|
||||
|
||||
def lining_lyric(self, text_template: dict, lyric_index: int, lyric_text: str, start_sec: float, end_sec: float) -> dict:
|
||||
duration = end_sec - start_sec
|
||||
text_scene = copy.deepcopy(text_template)
|
||||
text_scene["name"] = f"Caption-{lyric_index}"
|
||||
text_scene["duration"] = duration
|
||||
text_scene["time"] = start_sec
|
||||
text_scene["elements"][0]["name"] = f"lyric-{lyric_index}"
|
||||
text_scene["elements"][0]["text"] = lyric_text
|
||||
return text_scene
|
||||
|
||||
|
||||
def get_text_template(self):
|
||||
match self.orientation:
|
||||
case "vertical":
|
||||
return text_template_v_1
|
||||
case "horizontal":
|
||||
return text_template_h_1
|
||||
|
||||
|
|
|
|||
|
|
@ -197,19 +197,19 @@ class SunoService:
|
|||
"audio_id" : audio_id
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
response = await client.post(
|
||||
f"{self.BASE_URL}/generate/get-timestamped-lyrics",
|
||||
headers=self.headers,
|
||||
json = payload,
|
||||
json=payload,
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data is None:
|
||||
if not data or not data['data']:
|
||||
raise ValueError("Suno API returned empty response for task status")
|
||||
|
||||
return data['alignedWords']
|
||||
return data['data']['alignedWords']
|
||||
|
||||
def parse_status_response(self, result: dict | None) -> PollingSongResponse:
|
||||
"""Suno API 상태 응답을 파싱하여 PollingSongResponse로 변환합니다.
|
||||
|
|
@ -355,12 +355,12 @@ class SunoService:
|
|||
|
||||
results.append({
|
||||
'text': sentence,
|
||||
'startS': word_start['startS'],
|
||||
'endS': word_end['endS'],
|
||||
'start_sec': word_start['startS'],
|
||||
'end_sec': word_end['endS'],
|
||||
})
|
||||
|
||||
search_pos = found_pos + len(norm_sentence)
|
||||
else:
|
||||
results.append({'text': sentence, 'startS': None, 'endS': None})
|
||||
results.append({'text': sentence, 'start_sec': None, 'end_sec': None})
|
||||
|
||||
return results
|
||||
|
|
@ -27,7 +27,7 @@ from app.dependencies.pagination import (
|
|||
)
|
||||
from app.home.models import Image, Project
|
||||
from app.lyric.models import Lyric
|
||||
from app.song.models import Song
|
||||
from app.song.models import Song, SongTimestamp
|
||||
from app.video.models import Video
|
||||
from app.video.schemas.video_schema import (
|
||||
DownloadVideoResponse,
|
||||
|
|
@ -304,6 +304,19 @@ async def generate_video(
|
|||
)
|
||||
logger.debug(f"[generate_video] Duration extended to {creatomate_service.target_duration}s - task_id: {task_id}")
|
||||
|
||||
# 이런거 추가해야하는데 AI가 자꾸 번호 달면 제가 번호를 다 밀어야 하나요?
|
||||
|
||||
song_timestamp_result = await session.execute(
|
||||
select(SongTimestamp)
|
||||
.where(SongTimestamp.suno_audio_id == song.suno_audio_id)
|
||||
)
|
||||
song_timestamp_list = song_timestamp_result.scalars().all()
|
||||
|
||||
text_template = creatomate_service.get_text_template()
|
||||
for idx, aligned in enumerate(song_timestamp_list):
|
||||
caption = creatomate_service.lining_lyric(text_template, idx, aligned.lyric_line, aligned.start_time, aligned.end_time )
|
||||
final_template['source']['elements'].append(caption)
|
||||
|
||||
# 6-5. 커스텀 렌더링 요청 (비동기)
|
||||
render_response = await creatomate_service.make_creatomate_custom_call_async(
|
||||
final_template["source"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue