forked from 77media/video-flow
Merge branch 'dev' into prod
This commit is contained in:
commit
c9fc8f2da1
@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Dropdown } from "antd";
|
|
||||||
import { RectangleHorizontal, RectangleVertical } from "lucide-react";
|
import { RectangleHorizontal, RectangleVertical } from "lucide-react";
|
||||||
import { AspectRatioOptions } from "./types";
|
|
||||||
|
|
||||||
export type AspectRatioValue =
|
export type AspectRatioValue =
|
||||||
| "VIDEO_ASPECT_RATIO_LANDSCAPE"
|
| "VIDEO_ASPECT_RATIO_LANDSCAPE"
|
||||||
@ -22,13 +20,13 @@ interface AspectRatioSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable aspect ratio selector (landscape/portrait) using Antd Dropdown.
|
* Aspect ratio selector using Antd Radio.Group with two options: landscape and portrait.
|
||||||
* Shows an icon and label, and calls onChange when a new ratio is chosen.
|
* Uses icons as button content and triggers onChange when selection changes.
|
||||||
* @param {AspectRatioValue} value - current selected value
|
* @param {AspectRatioValue} value - current selected value
|
||||||
* @param {(v: AspectRatioValue) => void} onChange - change handler
|
* @param {(v: AspectRatioValue) => void} onChange - change handler
|
||||||
* @param {string} [className] - optional className for trigger button
|
* @param {string} [className] - optional className for wrapper
|
||||||
* @param {string} [placement] - Dropdown placement, default is top
|
* @param {string} [placement] - kept for backward compatibility (unused)
|
||||||
* @param {string} [dataAlt] - data-alt attribute for the trigger
|
* @param {string} [dataAlt] - data-alt attribute for the wrapper
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export const AspectRatioSelector = ({
|
export const AspectRatioSelector = ({
|
||||||
@ -39,45 +37,47 @@ export const AspectRatioSelector = ({
|
|||||||
dataAlt = "config-aspect-ratio",
|
dataAlt = "config-aspect-ratio",
|
||||||
}: AspectRatioSelectorProps) => {
|
}: AspectRatioSelectorProps) => {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<div
|
||||||
overlayClassName="aspect-dropdown"
|
data-alt={dataAlt}
|
||||||
menu={{
|
role="radiogroup"
|
||||||
items: AspectRatioOptions.map((option) => ({
|
aria-label="aspect-ratio"
|
||||||
key: option.value,
|
className={`inline-flex p-1 items-center rounded-[8px] bg-white/20 ${className || ""}`}
|
||||||
label: (
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-2 px-2 py-2 ${
|
|
||||||
option.value === value ? "bg-white/[0.12] rounded-md" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{option.value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? (
|
|
||||||
<RectangleHorizontal className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<RectangleVertical className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm text-white">{option.label}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
onClick: ({ key }) => onChange(key as AspectRatioValue),
|
|
||||||
}}
|
|
||||||
trigger={["click"]}
|
|
||||||
placement={placement}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
data-alt={dataAlt}
|
type="button"
|
||||||
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2 ${className || ""}`}
|
role="radio"
|
||||||
|
aria-checked={value === "VIDEO_ASPECT_RATIO_LANDSCAPE"}
|
||||||
|
onClick={() => onChange("VIDEO_ASPECT_RATIO_LANDSCAPE")}
|
||||||
|
className="outline-none rounded-[8px]"
|
||||||
>
|
>
|
||||||
{value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? (
|
<div
|
||||||
<RectangleHorizontal className={"w-4 h-4"} />
|
className={`flex items-center justify-center px-1 py-0.5 ${
|
||||||
) : (
|
value === "VIDEO_ASPECT_RATIO_LANDSCAPE"
|
||||||
<RectangleVertical className={"w-4 h-4"} />
|
? "bg-white/60 text-black rounded-[4px]"
|
||||||
)}
|
: "bg-transparent text-white"
|
||||||
<span className="text-sm">
|
}`}
|
||||||
{value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? "16:9" : "9:16"}
|
>
|
||||||
</span>
|
<RectangleHorizontal className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={value === "VIDEO_ASPECT_RATIO_PORTRAIT"}
|
||||||
|
onClick={() => onChange("VIDEO_ASPECT_RATIO_PORTRAIT")}
|
||||||
|
className="outline-none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center px-1 py-0.5 ${
|
||||||
|
value === "VIDEO_ASPECT_RATIO_PORTRAIT"
|
||||||
|
? "bg-white/60 text-black rounded-[4px]"
|
||||||
|
: "bg-transparent text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RectangleVertical className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -523,11 +523,12 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
data-alt={`config-video-duration`}
|
data-alt="config-video-duration"
|
||||||
className={`flex items-center gap-1 text-white/80 transition-all duration-200 ${isMobile ? 'px-1' : 'px-2'} py-2`}
|
className={`flex items-center gap-1 text-white/80 transition-all duration-200 ${isMobile ? 'px-1' : 'px-2'} py-2`}
|
||||||
>
|
>
|
||||||
<Clock className={"w-4 h-4"} />
|
<Clock className={"w-4 h-4"} />
|
||||||
<span className="text-sm">{configOptions.videoDuration === 'unlimited' ? 'auto' : (isMobile ? configOptions.videoDuration.replace('min', 'm') : configOptions.videoDuration)}</span>
|
<span className="text-sm">{configOptions.videoDuration === 'unlimited' ? 'auto' : (isMobile ? configOptions.videoDuration.replace('min', 'm') : configOptions.videoDuration)}</span>
|
||||||
|
<ChevronUp className="w-3 h-3 text-white/60" />
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
|
|||||||
@ -138,9 +138,11 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleTestExportRef.current?.();
|
handleTestExportRef.current?.();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
const title = isMobile ? 'editing...' : 'Performing intelligent editing...';
|
||||||
|
|
||||||
// 显示进度提示并启动超时定时器
|
// 显示进度提示并启动超时定时器
|
||||||
emitToastShow({ title: 'Performing intelligent editing...', progress: 0 });
|
emitToastShow({ title: title, progress: 0 });
|
||||||
// 启动自动推进到 90% 的进度(8分钟)
|
// 启动自动推进到 90% 的进度(8分钟)
|
||||||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressStartRef.current = Date.now();
|
editingProgressStartRef.current = Date.now();
|
||||||
|
|||||||
@ -286,7 +286,7 @@ export function H5MediaViewer({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div data-alt="script-content" className="w-full h-full pt-[4rem]">
|
<div data-alt="script-content" className="w-full h-full">
|
||||||
{scriptData ? (
|
{scriptData ? (
|
||||||
<>
|
<>
|
||||||
<ScriptRenderer
|
<ScriptRenderer
|
||||||
|
|||||||
@ -677,9 +677,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
)}
|
)}
|
||||||
{currentSketch.status === 2 && (
|
{currentSketch.status === 2 && (
|
||||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center">
|
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center">
|
||||||
<div className="text-[#813b9dcc] text-2xl font-bold flex items-center gap-2">
|
<div className="text-2xl mb-4">⚠️</div>
|
||||||
<CircleAlert className="w-10 h-10" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||||||
|
|||||||
@ -298,10 +298,8 @@ export function ThumbnailGrid({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sketch.status === 2 && (
|
{sketch.status === 2 && (
|
||||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center">
|
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center z-20">
|
||||||
<div className="text-[#813b9dcc] text-xl font-bold flex items-center gap-2">
|
<div className="text-2xl mb-4">⚠️</div>
|
||||||
<CircleAlert className="w-10 h-10" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
|||||||
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||||
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
||||||
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||||
|
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||||
|
|
||||||
interface UseWorkflowDataProps {
|
interface UseWorkflowDataProps {
|
||||||
onEditPlanGenerated?: () => void;
|
onEditPlanGenerated?: () => void;
|
||||||
@ -37,6 +38,8 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isMobile, isTablet, isDesktop } = useDeviceType();
|
||||||
|
|
||||||
const cutUrl = process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai';
|
const cutUrl = process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai';
|
||||||
console.log('cutUrl', cutUrl);
|
console.log('cutUrl', cutUrl);
|
||||||
|
|
||||||
@ -158,7 +161,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 显示生成剪辑计划进度提示
|
// 显示生成剪辑计划进度提示
|
||||||
emitToastShow({ title: `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 });
|
emitToastShow({ title: isMobile ? 'Preparing for editing...' : `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 });
|
||||||
// 平滑推进到 80%,后续阶段接管
|
// 平滑推进到 80%,后续阶段接管
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const duration = 3 * 60 * 1000; // 3分钟推进到 80%
|
const duration = 3 * 60 * 1000; // 3分钟推进到 80%
|
||||||
@ -198,7 +201,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
setIsGenerateEditPlan(false);
|
setIsGenerateEditPlan(false);
|
||||||
|
|
||||||
// 显示失败提示,并在稍后隐藏
|
// 显示失败提示,并在稍后隐藏
|
||||||
emitToastShow({ title: 'Editing plan generation failed. Retrying later.', progress: 0 });
|
// emitToastShow({ title: isMobile ? 'Editing plan generation failed. Retrying later.' : 'Editing plan generation failed. Retrying later.', progress: 0 });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emitToastHide();
|
emitToastHide();
|
||||||
setIsLoadingGenerateEditPlan(false);
|
setIsLoadingGenerateEditPlan(false);
|
||||||
@ -419,7 +422,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) {
|
if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) {
|
||||||
setIsAnalyzing(true);
|
setIsAnalyzing(true);
|
||||||
// 显示准备剪辑计划的提示
|
// 显示准备剪辑计划的提示
|
||||||
emitToastShow({ title: 'Preparing intelligent editing plan...', progress: 0 });
|
emitToastShow({ title: isMobile ? 'Preparing for editing...' : 'Preparing intelligent editing plan...', progress: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
|
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
|
||||||
|
|||||||
@ -10,7 +10,14 @@
|
|||||||
*/
|
*/
|
||||||
const localPost = async <T>(url: string, data: any): Promise<T> => {
|
const localPost = async <T>(url: string, data: any): Promise<T> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
// 使用环境变量中的 BASE_URL(生产要求使用 NEXT_PUBLIC_BASE_URL)
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || '';
|
||||||
|
const isAbsolute = /^https?:\/\//i.test(url);
|
||||||
|
const normalizedBase = baseUrl.replace(/\/$/, '');
|
||||||
|
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
|
||||||
|
const fullUrl = isAbsolute ? url : `${normalizedBase}${normalizedPath}`;
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user