배경음악 재시도 오류 수정 및 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",
"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)",

View File

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

View File

@ -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<CompletionContentProps> = ({
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<CompletionContentProps> = ({
return <p className="comp2-lyrics-text">{t('completion.noLyricsBGM')}</p>;
}
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 (
<div className="comp2-lyrics-paragraphs">

View File

@ -153,10 +153,10 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
'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<SoundStudioContentProps> = ({
<div className="lyrics-display">
{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 (
<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 {
success: boolean;
task_id: string;

View File

@ -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<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
export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise<VideoGenerateResponse> {
const response = await authenticatedFetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, {