From f3195628d2376101f03c58aeb8f4cbcf8e1e886c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Wed, 20 May 2026 13:21:22 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B0=EA=B2=BD=EC=9D=8C=EC=95=85=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20subtitle=20=ED=99=95=EC=9D=B8=EC=9D=84?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/en.json | 2 + src/locales/ko.json | 2 + src/pages/Dashboard/CompletionContent.tsx | 25 +++++++++--- src/pages/Dashboard/SoundStudioContent.tsx | 17 ++++---- src/types/api.ts | 7 ++++ src/utils/api.ts | 47 ++++++++++++++++++++++ 6 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 848bfa7..37c540e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -266,6 +266,8 @@ "titleError": "Video Generation Failed", "titleComplete": "Content Creation Complete", "imageAndVideo": "Images & Video", + "checkingSubtitle": "Checking subtitle generation status...", + "waitingSubtitle": "Generating subtitles...", "requestingGeneration": "Requesting video generation...", "generatingVideo": "Generating video...", "processingAfterRefresh": "Processing video... (recovered after refresh)", diff --git a/src/locales/ko.json b/src/locales/ko.json index aee7f29..c8abca8 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -266,6 +266,8 @@ "titleError": "영상 생성 실패", "titleComplete": "콘텐츠 제작 완료", "imageAndVideo": "이미지 및 영상", + "checkingSubtitle": "자막 생성 상태를 확인하고 있습니다...", + "waitingSubtitle": "자막을 생성하고 있습니다...", "requestingGeneration": "영상 생성을 요청하고 있습니다...", "generatingVideo": "영상을 생성하고 있습니다...", "processingAfterRefresh": "영상을 처리하고 있습니다... (새로고침 후 복구됨)", diff --git a/src/pages/Dashboard/CompletionContent.tsx b/src/pages/Dashboard/CompletionContent.tsx index a37e066..4f8f0ce 100755 --- a/src/pages/Dashboard/CompletionContent.tsx +++ b/src/pages/Dashboard/CompletionContent.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { generateVideo, waitForVideoComplete } from '../../utils/api'; +import { generateVideo, waitForVideoComplete, getSubtitleStatus, waitForSubtitleComplete } from '../../utils/api'; import SocialPostingModal from '../../components/SocialPostingModal'; import { useTutorial } from '../../components/Tutorial/useTutorial'; import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps'; @@ -174,10 +174,19 @@ const CompletionContent: React.FC = ({ hasStartedGeneration.current = true; setVideoStatus('generating'); - setStatusMessage(t('completion.requestingGeneration')); + setStatusMessage(t('completion.checkingSubtitle')); setErrorMessage(null); try { + // 자막 완료 여부 확인 후 미완료면 폴링 + const subtitleStatus = await getSubtitleStatus(songTaskId); + if (subtitleStatus.status !== 'completed') { + setStatusMessage(t('completion.waitingSubtitle')); + await waitForSubtitleComplete(songTaskId); + } + + setStatusMessage(t('completion.requestingGeneration')); + const savedRatio = localStorage.getItem('castad_video_ratio'); const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical'; @@ -491,11 +500,15 @@ const CompletionContent: React.FC = ({ return

{t('completion.noLyricsBGM')}

; } const lines = songCompletionData.lyrics.split('\n').filter((l: string) => l.trim()); - const size = Math.ceil(lines.length / 3); + const intro = lines.slice(0, 1); + const outro = lines.slice(-1); + const body = lines.slice(1, -1); + const half = Math.ceil(body.length / 2); const sections = [ - { tag: '[Verse]', lines: lines.slice(0, size) }, - { tag: '[Chorus]', lines: lines.slice(size, size * 2) }, - { tag: '[Outro]', lines: lines.slice(size * 2) }, + { tag: '[Intro]', lines: intro }, + { tag: '[Verse]', lines: body.slice(0, half) }, + { tag: '[Chorus]', lines: body.slice(half) }, + { tag: '[Outro]', lines: outro }, ].filter(s => s.lines.length > 0); return (
diff --git a/src/pages/Dashboard/SoundStudioContent.tsx b/src/pages/Dashboard/SoundStudioContent.tsx index 0020605..1d6688e 100755 --- a/src/pages/Dashboard/SoundStudioContent.tsx +++ b/src/pages/Dashboard/SoundStudioContent.tsx @@ -153,10 +153,10 @@ const SoundStudioContent: React.FC = ({ 'ROCK': 'rock', }; + const isInstrumental = selectedType === '배경음악'; const songResponse = await generateSong(imageTaskId, { genre: genreMap[selectedGenre] || 'pop', - language, - lyrics: currentLyrics, + ...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: currentLyrics }), }); if (!songResponse.success) { @@ -606,12 +606,15 @@ const SoundStudioContent: React.FC = ({
{lyrics ? (() => { const lines = lyrics.split('\n').filter(l => l.trim()); - const size = Math.ceil(lines.length / 3); - const outroLines = lines.slice(size * 2).slice(0, 2); + const intro = lines.slice(0, 1); + const outro = lines.slice(-1); + const body = lines.slice(1, -1); + const half = Math.ceil(body.length / 2); const sections = [ - { tag: '[Verse]', lines: lines.slice(0, size) }, - { tag: '[Chorus]', lines: lines.slice(size, size * 2) }, - { tag: '[Outro]', lines: outroLines }, + { tag: '[Intro]', lines: intro }, + { tag: '[Verse]', lines: body.slice(0, half) }, + { tag: '[Chorus]', lines: body.slice(half) }, + { tag: '[Outro]', lines: outro }, ].filter(s => s.lines.length > 0); return (
diff --git a/src/types/api.ts b/src/types/api.ts index 504b234..f3493d0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -143,6 +143,13 @@ export interface SongDownloadResponse { } // 영상 생성 응답 +// 자막 상태 확인 응답 +export interface SubtitleStatusResponse { + task_id: string; + status: string; // 'pending' | 'processing' | 'completed' | 'failed' | 'error' + message: string; +} + export interface VideoGenerateResponse { success: boolean; task_id: string; diff --git a/src/utils/api.ts b/src/utils/api.ts index f9dced3..e133efd 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -8,6 +8,7 @@ import { SongGenerateResponse, SongStatusResponse, SongDownloadResponse, + SubtitleStatusResponse, VideoGenerateResponse, VideoStatusResponse, VideoDownloadResponse, @@ -245,6 +246,52 @@ export async function waitForSongComplete( return poll(); } +// 자막 상태 확인 API +export async function getSubtitleStatus(taskId: string): Promise { + const response = await authenticatedFetch(`${API_URL}/lyric/subtitle/status/${taskId}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} + +// 자막 완료까지 폴링 (5초 간격, 10분 타임아웃) +const SUBTITLE_POLL_INTERVAL = 5000; +const SUBTITLE_POLL_TIMEOUT = 5 * 60 * 1000; + +export async function waitForSubtitleComplete( + taskId: string, + onStatusChange?: (status: string) => void +): Promise { + const startTime = Date.now(); + + const poll = async (): Promise => { + if (Date.now() - startTime > SUBTITLE_POLL_TIMEOUT) { + throw new Error('TIMEOUT'); + } + + const response = await getSubtitleStatus(taskId); + onStatusChange?.(response.status); + + if (response.status === 'completed') { + return response; + } + + if (response.status === 'failed' || response.status === 'error') { + throw new Error(response.message || '자막 생성에 실패했습니다.'); + } + + await new Promise(resolve => setTimeout(resolve, SUBTITLE_POLL_INTERVAL)); + return poll(); + }; + + return poll(); +} + // 영상 생성 API export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise { const response = await authenticatedFetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, {