배경음악 재시도 오류 수정 및 subtitle 확인을 위한 엔드포인트 추가
parent
9910d0146c
commit
f3195628d2
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -266,6 +266,8 @@
|
|||
"titleError": "영상 생성 실패",
|
||||
"titleComplete": "콘텐츠 제작 완료",
|
||||
"imageAndVideo": "이미지 및 영상",
|
||||
"checkingSubtitle": "자막 생성 상태를 확인하고 있습니다...",
|
||||
"waitingSubtitle": "자막을 생성하고 있습니다...",
|
||||
"requestingGeneration": "영상 생성을 요청하고 있습니다...",
|
||||
"generatingVideo": "영상을 생성하고 있습니다...",
|
||||
"processingAfterRefresh": "영상을 처리하고 있습니다... (새로고침 후 복구됨)",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}`, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue