배경음악 재시도 오류 수정 및 subtitle 확인을 위한 엔드포인트 추가

feature-ADO2
김성경 2026-05-20 13:21:22 +09:00
parent 9910d0146c
commit f3195628d2
6 changed files with 87 additions and 13 deletions

View File

@ -266,6 +266,8 @@
"titleError": "Video Generation Failed", "titleError": "Video Generation Failed",
"titleComplete": "Content Creation Complete", "titleComplete": "Content Creation Complete",
"imageAndVideo": "Images & Video", "imageAndVideo": "Images & Video",
"checkingSubtitle": "Checking subtitle generation status...",
"waitingSubtitle": "Generating subtitles...",
"requestingGeneration": "Requesting video generation...", "requestingGeneration": "Requesting video generation...",
"generatingVideo": "Generating video...", "generatingVideo": "Generating video...",
"processingAfterRefresh": "Processing video... (recovered after refresh)", "processingAfterRefresh": "Processing video... (recovered after refresh)",

View File

@ -266,6 +266,8 @@
"titleError": "영상 생성 실패", "titleError": "영상 생성 실패",
"titleComplete": "콘텐츠 제작 완료", "titleComplete": "콘텐츠 제작 완료",
"imageAndVideo": "이미지 및 영상", "imageAndVideo": "이미지 및 영상",
"checkingSubtitle": "자막 생성 상태를 확인하고 있습니다...",
"waitingSubtitle": "자막을 생성하고 있습니다...",
"requestingGeneration": "영상 생성을 요청하고 있습니다...", "requestingGeneration": "영상 생성을 요청하고 있습니다...",
"generatingVideo": "영상을 생성하고 있습니다...", "generatingVideo": "영상을 생성하고 있습니다...",
"processingAfterRefresh": "영상을 처리하고 있습니다... (새로고침 후 복구됨)", "processingAfterRefresh": "영상을 처리하고 있습니다... (새로고침 후 복구됨)",

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; 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 SocialPostingModal from '../../components/SocialPostingModal';
import { useTutorial } from '../../components/Tutorial/useTutorial'; import { useTutorial } from '../../components/Tutorial/useTutorial';
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps'; import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
@ -174,10 +174,19 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
hasStartedGeneration.current = true; hasStartedGeneration.current = true;
setVideoStatus('generating'); setVideoStatus('generating');
setStatusMessage(t('completion.requestingGeneration')); setStatusMessage(t('completion.checkingSubtitle'));
setErrorMessage(null); setErrorMessage(null);
try { 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 savedRatio = localStorage.getItem('castad_video_ratio');
const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical'; const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical';
@ -491,11 +500,15 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
return <p className="comp2-lyrics-text">{t('completion.noLyricsBGM')}</p>; return <p className="comp2-lyrics-text">{t('completion.noLyricsBGM')}</p>;
} }
const lines = songCompletionData.lyrics.split('\n').filter((l: string) => l.trim()); 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 = [ const sections = [
{ tag: '[Verse]', lines: lines.slice(0, size) }, { tag: '[Intro]', lines: intro },
{ tag: '[Chorus]', lines: lines.slice(size, size * 2) }, { tag: '[Verse]', lines: body.slice(0, half) },
{ tag: '[Outro]', lines: lines.slice(size * 2) }, { tag: '[Chorus]', lines: body.slice(half) },
{ tag: '[Outro]', lines: outro },
].filter(s => s.lines.length > 0); ].filter(s => s.lines.length > 0);
return ( return (
<div className="comp2-lyrics-paragraphs"> <div className="comp2-lyrics-paragraphs">

View File

@ -153,10 +153,10 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
'ROCK': 'rock', 'ROCK': 'rock',
}; };
const isInstrumental = selectedType === '배경음악';
const songResponse = await generateSong(imageTaskId, { const songResponse = await generateSong(imageTaskId, {
genre: genreMap[selectedGenre] || 'pop', genre: genreMap[selectedGenre] || 'pop',
language, ...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: currentLyrics }),
lyrics: currentLyrics,
}); });
if (!songResponse.success) { if (!songResponse.success) {
@ -606,12 +606,15 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
<div className="lyrics-display"> <div className="lyrics-display">
{lyrics ? (() => { {lyrics ? (() => {
const lines = lyrics.split('\n').filter(l => l.trim()); const lines = lyrics.split('\n').filter(l => l.trim());
const size = Math.ceil(lines.length / 3); const intro = lines.slice(0, 1);
const outroLines = lines.slice(size * 2).slice(0, 2); const outro = lines.slice(-1);
const body = lines.slice(1, -1);
const half = Math.ceil(body.length / 2);
const sections = [ const sections = [
{ tag: '[Verse]', lines: lines.slice(0, size) }, { tag: '[Intro]', lines: intro },
{ tag: '[Chorus]', lines: lines.slice(size, size * 2) }, { tag: '[Verse]', lines: body.slice(0, half) },
{ tag: '[Outro]', lines: outroLines }, { tag: '[Chorus]', lines: body.slice(half) },
{ tag: '[Outro]', lines: outro },
].filter(s => s.lines.length > 0); ].filter(s => s.lines.length > 0);
return ( return (
<div className="lyrics-paragraphs"> <div className="lyrics-paragraphs">

View File

@ -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 { export interface VideoGenerateResponse {
success: boolean; success: boolean;
task_id: string; task_id: string;

View File

@ -8,6 +8,7 @@ import {
SongGenerateResponse, SongGenerateResponse,
SongStatusResponse, SongStatusResponse,
SongDownloadResponse, SongDownloadResponse,
SubtitleStatusResponse,
VideoGenerateResponse, VideoGenerateResponse,
VideoStatusResponse, VideoStatusResponse,
VideoDownloadResponse, VideoDownloadResponse,
@ -245,6 +246,52 @@ export async function waitForSongComplete(
return poll(); return poll();
} }
// 자막 상태 확인 API
export async function getSubtitleStatus(taskId: string): Promise<SubtitleStatusResponse> {
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<SubtitleStatusResponse> {
const startTime = Date.now();
const poll = async (): Promise<SubtitleStatusResponse> => {
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 // 영상 생성 API
export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise<VideoGenerateResponse> { export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise<VideoGenerateResponse> {
const response = await authenticatedFetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, { const response = await authenticatedFetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, {