forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
541568f0f5
@ -987,7 +987,7 @@ export const batchUpdateVideoSegments = async (request: {
|
|||||||
/** 新的视频地址列表 */
|
/** 新的视频地址列表 */
|
||||||
video_urls: string[];
|
video_urls: string[];
|
||||||
/** 新的状态 0:视频加载中 1:任务已完成 2:任务失败 */
|
/** 新的状态 0:视频加载中 1:任务已完成 2:任务失败 */
|
||||||
status: number;
|
status: number | null;
|
||||||
/** 优化后的描述文本 */
|
/** 优化后的描述文本 */
|
||||||
optimized_description?: string;
|
optimized_description?: string;
|
||||||
/** 关键词列表 */
|
/** 关键词列表 */
|
||||||
@ -1033,7 +1033,8 @@ export const getCharacterShots = async (request: {
|
|||||||
video_id: string;
|
video_id: string;
|
||||||
video_status: number|null;
|
video_status: number|null;
|
||||||
}[];
|
}[];
|
||||||
|
/** 视频状态 */
|
||||||
|
video_status: number|null;
|
||||||
}>;
|
}>;
|
||||||
/** 总数量 */
|
/** 总数量 */
|
||||||
total_count: number;
|
total_count: number;
|
||||||
|
|||||||
@ -98,7 +98,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
|||||||
name: `视频片段_${scene.video_id}`,
|
name: `视频片段_${scene.video_id}`,
|
||||||
sketchUrl: "",
|
sketchUrl: "",
|
||||||
videoUrl: scene.video_urls,// 保持为string[]类型
|
videoUrl: scene.video_urls,// 保持为string[]类型
|
||||||
status:scene.video_urls.length>0?1:0, // 默认为已完成状态
|
status: scene.video_status !== null? scene.video_status : scene.video_urls.length>0?1:0, // 默认为已完成状态
|
||||||
lens: [],
|
lens: [],
|
||||||
selected: false,
|
selected: false,
|
||||||
applied: true // 由于是通过角色查询到的,所以都是已应用的
|
applied: true // 由于是通过角色查询到的,所以都是已应用的
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
MatchedPerson,
|
MatchedPerson,
|
||||||
RoleRecognitionResponse
|
RoleRecognitionResponse
|
||||||
} from "@/api/DTO/movieEdit";
|
} from "@/api/DTO/movieEdit";
|
||||||
import { VideoSegmentEntity } from "../domain/Entities";
|
import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities";
|
||||||
import { LensType, SimpleCharacter } from "../domain/valueObject";
|
import { LensType, SimpleCharacter } from "../domain/valueObject";
|
||||||
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
||||||
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
|
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
|
||||||
@ -21,6 +21,8 @@ export interface UseShotService {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
/** 视频片段列表 */
|
/** 视频片段列表 */
|
||||||
videoSegments: VideoSegmentEntity[];
|
videoSegments: VideoSegmentEntity[];
|
||||||
|
/** 剧本中角色列表 */
|
||||||
|
scriptRoles: ScriptRoleEntity[];
|
||||||
/** 当前选中的视频片段 */
|
/** 当前选中的视频片段 */
|
||||||
selectedSegment: VideoSegmentEntity | null;
|
selectedSegment: VideoSegmentEntity | null;
|
||||||
/** 识别出的人物信息 */
|
/** 识别出的人物信息 */
|
||||||
@ -69,6 +71,7 @@ export const useShotService = (): UseShotService => {
|
|||||||
// 响应式状态
|
// 响应式状态
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [videoSegments, setVideoSegments] = useState<VideoSegmentEntity[]>([]);
|
const [videoSegments, setVideoSegments] = useState<VideoSegmentEntity[]>([]);
|
||||||
|
const [scriptRoles, setScriptRoles] = useState<ScriptRoleEntity[]>([]);
|
||||||
const [selectedSegment, setSelectedSegment] =
|
const [selectedSegment, setSelectedSegment] =
|
||||||
useState<VideoSegmentEntity | null>(null);
|
useState<VideoSegmentEntity | null>(null);
|
||||||
const [projectId, setProjectId] = useState<string>("");
|
const [projectId, setProjectId] = useState<string>("");
|
||||||
@ -90,9 +93,10 @@ export const useShotService = (): UseShotService => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const segments = await vidoEditUseCase.getVideoSegmentList(projectId);
|
const { segments, roles } = await vidoEditUseCase.getVideoSegmentList(projectId);
|
||||||
setProjectId(projectId);
|
setProjectId(projectId);
|
||||||
setVideoSegments(segments);
|
setVideoSegments(segments);
|
||||||
|
setScriptRoles(roles);
|
||||||
setIntervalIdHandler(projectId);
|
setIntervalIdHandler(projectId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取视频片段列表失败:", error);
|
console.error("获取视频片段列表失败:", error);
|
||||||
@ -112,7 +116,7 @@ export const useShotService = (): UseShotService => {
|
|||||||
// 定义定时任务,每5秒执行一次
|
// 定义定时任务,每5秒执行一次
|
||||||
const newIntervalId = setInterval(async () => {
|
const newIntervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const segments = await vidoEditUseCase.getVideoSegmentList(projectId,()=>{
|
const { segments } = await vidoEditUseCase.getVideoSegmentList(projectId,()=>{
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
setIntervalId(null);
|
setIntervalId(null);
|
||||||
@ -474,6 +478,7 @@ export const useShotService = (): UseShotService => {
|
|||||||
// 响应式状态
|
// 响应式状态
|
||||||
loading,
|
loading,
|
||||||
videoSegments,
|
videoSegments,
|
||||||
|
scriptRoles,
|
||||||
selectedSegment,
|
selectedSegment,
|
||||||
matched_persons,
|
matched_persons,
|
||||||
// 操作方法
|
// 操作方法
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
/**============因协同任务开发流程没有明确管理,导致的必要的适配=================**/
|
/**============因协同任务开发流程没有明确管理,导致的必要的适配=================**/
|
||||||
|
|
||||||
import { VideoSegmentEntity } from "../domain/Entities";
|
import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities";
|
||||||
import { LensType, ContentItem } from "../domain/valueObject";
|
import { LensType, ContentItem } from "../domain/valueObject";
|
||||||
|
|
||||||
|
|
||||||
@ -73,6 +73,7 @@ export class VideoSegmentEntityAdapter {
|
|||||||
video_status: number|null;
|
video_status: number|null;
|
||||||
}>;
|
}>;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
project_characters: ScriptRoleEntity[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 解析shotContent,分离镜头描述和对话内容
|
* @description 解析shotContent,分离镜头描述和对话内容
|
||||||
@ -208,7 +209,7 @@ export class VideoSegmentEntityAdapter {
|
|||||||
// 如果有对话内容,添加到镜头描述后面
|
// 如果有对话内容,添加到镜头描述后面
|
||||||
if (lensItem.content && lensItem.content.length > 0) {
|
if (lensItem.content && lensItem.content.length > 0) {
|
||||||
const dialogueLines = lensItem.content.map(dialogue =>
|
const dialogueLines = lensItem.content.map(dialogue =>
|
||||||
`${dialogue.roleName} [CH-01]: ${dialogue.content}`
|
`${dialogue.roleName} : ${dialogue.content}`
|
||||||
);
|
);
|
||||||
fullContent += '\n' + dialogueLines.join('\n');
|
fullContent += '\n' + dialogueLines.join('\n');
|
||||||
}
|
}
|
||||||
@ -312,7 +313,7 @@ export class VideoSegmentEntityAdapter {
|
|||||||
// 如果有对话内容,添加到镜头描述后面
|
// 如果有对话内容,添加到镜头描述后面
|
||||||
if (lensType.content && lensType.content.length > 0) {
|
if (lensType.content && lensType.content.length > 0) {
|
||||||
const dialogueLines = lensType.content.map(dialogue =>
|
const dialogueLines = lensType.content.map(dialogue =>
|
||||||
`${dialogue.roleName} [CH-01]: ${dialogue.content}`
|
`${dialogue.roleName} : ${dialogue.content}`
|
||||||
);
|
);
|
||||||
fullContent += '\n' + dialogueLines.join('\n');
|
fullContent += '\n' + dialogueLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject';
|
import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject';
|
||||||
|
import { ScriptRoleEntity } from '../domain/Entities';
|
||||||
// 定义角色属性接口
|
|
||||||
interface CharacterAttributes {
|
|
||||||
name: string;
|
|
||||||
// gender: string;
|
|
||||||
// age: string;
|
|
||||||
avatar: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义高亮属性接口
|
// 定义高亮属性接口
|
||||||
interface HighlightAttributes {
|
interface HighlightAttributes {
|
||||||
@ -23,7 +16,7 @@ interface TextNode {
|
|||||||
// 定义角色标记节点接口
|
// 定义角色标记节点接口
|
||||||
interface CharacterTokenNode {
|
interface CharacterTokenNode {
|
||||||
type: 'characterToken';
|
type: 'characterToken';
|
||||||
attrs: CharacterAttributes;
|
attrs: ScriptRoleEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义高亮节点接口
|
// 定义高亮节点接口
|
||||||
@ -55,7 +48,7 @@ export class TextToShotAdapter {
|
|||||||
* @param roles 角色列表
|
* @param roles 角色列表
|
||||||
* @returns ContentNode[] 节点数组
|
* @returns ContentNode[] 节点数组
|
||||||
*/
|
*/
|
||||||
public static parseText(text: string, roles: SimpleCharacter[]): ContentNode[] {
|
public static parseText(text: string, roles: ScriptRoleEntity[]): ContentNode[] {
|
||||||
const nodes: ContentNode[] = [];
|
const nodes: ContentNode[] = [];
|
||||||
let currentText = text;
|
let currentText = text;
|
||||||
|
|
||||||
@ -63,31 +56,32 @@ export class TextToShotAdapter {
|
|||||||
// 既要兼容 每个单词 首字母大写 其余小写、还要兼容 全部大写
|
// 既要兼容 每个单词 首字母大写 其余小写、还要兼容 全部大写
|
||||||
const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length).map(role => ({
|
const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length).map(role => ({
|
||||||
...role,
|
...role,
|
||||||
name: role.name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ')
|
match_name: role.name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ') + ' [' + role.id + ']'
|
||||||
})).concat([...roles].map(role => ({
|
})).concat([...roles].map(role => ({
|
||||||
...role,
|
...role,
|
||||||
name: role.name.toUpperCase()
|
match_name: role.name.toUpperCase() + ' [' + role.id + ']'
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
console.log('shots-匹配角色', text, sortedRoles);
|
||||||
|
|
||||||
|
|
||||||
while (currentText.length > 0) {
|
while (currentText.length > 0) {
|
||||||
let matchFound = false;
|
let matchFound = false;
|
||||||
|
|
||||||
// 尝试匹配角色
|
// 尝试匹配角色
|
||||||
for (const role of sortedRoles) {
|
for (const role of sortedRoles) {
|
||||||
if (currentText.startsWith(role.name)) {
|
if (currentText.startsWith(role.match_name)) {
|
||||||
// 如果当前文本以角色名开头
|
// 如果当前文本以角色名开头
|
||||||
if (currentText.length > role.name.length) {
|
if (currentText.length > role.match_name.length) {
|
||||||
// 添加角色标记节点
|
// 添加角色标记节点
|
||||||
nodes.push({
|
nodes.push({
|
||||||
type: 'characterToken',
|
type: 'characterToken',
|
||||||
attrs: {
|
attrs: {
|
||||||
name: role.name,
|
...role
|
||||||
avatar: role.imageUrl
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 移除已处理的角色名
|
// 移除已处理的角色名
|
||||||
currentText = currentText.slice(role.name.length);
|
currentText = currentText.slice(role.match_name.length);
|
||||||
matchFound = true;
|
matchFound = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -99,7 +93,7 @@ export class TextToShotAdapter {
|
|||||||
// 查找下一个可能的角色名位置
|
// 查找下一个可能的角色名位置
|
||||||
let nextRoleIndex = currentText.length;
|
let nextRoleIndex = currentText.length;
|
||||||
for (const role of sortedRoles) {
|
for (const role of sortedRoles) {
|
||||||
const index = currentText.indexOf(role.name);
|
const index = currentText.indexOf(role.match_name);
|
||||||
if (index !== -1 && index < nextRoleIndex) {
|
if (index !== -1 && index < nextRoleIndex) {
|
||||||
nextRoleIndex = index;
|
nextRoleIndex = index;
|
||||||
}
|
}
|
||||||
@ -196,7 +190,7 @@ export class TextToShotAdapter {
|
|||||||
* @param lensType LensType 实例
|
* @param lensType LensType 实例
|
||||||
* @returns Paragraph 格式的数据
|
* @returns Paragraph 格式的数据
|
||||||
*/
|
*/
|
||||||
public static fromLensType(lensType: LensType, roles: SimpleCharacter[]): Shot {
|
public static fromLensType(lensType: LensType, roles: ScriptRoleEntity[]): Shot {
|
||||||
const shotDescContent: Paragraph[] = [];
|
const shotDescContent: Paragraph[] = [];
|
||||||
const shotDialogsContent: Paragraph[] = [];
|
const shotDialogsContent: Paragraph[] = [];
|
||||||
|
|
||||||
@ -213,16 +207,15 @@ export class TextToShotAdapter {
|
|||||||
lensType.content.forEach(item => {
|
lensType.content.forEach(item => {
|
||||||
const dialogNodes = TextToShotAdapter.parseText(item.content, roles);
|
const dialogNodes = TextToShotAdapter.parseText(item.content, roles);
|
||||||
|
|
||||||
// 确保对话内容以角色标记开始
|
// 确保对话内容以角色标记开始 角色名都大写再匹配
|
||||||
const roleMatch = roles.find(role => role.name === item.roleName);
|
const roleMatch = roles.find(role => role.name.toUpperCase().includes(item.roleName.toUpperCase()));
|
||||||
if (roleMatch) {
|
if (roleMatch) {
|
||||||
const dialogContent: Paragraph = {
|
const dialogContent: Paragraph = {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [{
|
content: [{
|
||||||
type: 'characterToken',
|
type: 'characterToken',
|
||||||
attrs: {
|
attrs: {
|
||||||
name: roleMatch.name,
|
...roleMatch
|
||||||
avatar: roleMatch.imageUrl
|
|
||||||
}},
|
}},
|
||||||
...dialogNodes
|
...dialogNodes
|
||||||
]
|
]
|
||||||
@ -263,7 +256,7 @@ export class TextToShotAdapter {
|
|||||||
currentScript += node.text;
|
currentScript += node.text;
|
||||||
}
|
}
|
||||||
if (node.type === 'characterToken') {
|
if (node.type === 'characterToken') {
|
||||||
currentScript += node.attrs.name;
|
currentScript = currentScript + node.attrs.name + ' ' + node.attrs.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -282,10 +275,10 @@ export class TextToShotAdapter {
|
|||||||
if (node.type === 'characterToken') {
|
if (node.type === 'characterToken') {
|
||||||
// 记录说话角色的名称
|
// 记录说话角色的名称
|
||||||
if (!firstFindRole) {
|
if (!firstFindRole) {
|
||||||
dialogRoleName = node.attrs.name;
|
dialogRoleName = node.attrs.name + ' [' + node.attrs.id + ']';
|
||||||
firstFindRole = true;
|
firstFindRole = true;
|
||||||
} else {
|
} else {
|
||||||
dialogContent += node.attrs.name;
|
dialogContent += node.attrs.name + ' [' + node.attrs.id + ']';
|
||||||
}
|
}
|
||||||
} else if (node.type === 'text') {
|
} else if (node.type === 'text') {
|
||||||
// 累积对话内容
|
// 累积对话内容
|
||||||
@ -329,7 +322,7 @@ export class TextToShotAdapter {
|
|||||||
} else if (node.type === 'text') {
|
} else if (node.type === 'text') {
|
||||||
text += node.text;
|
text += node.text;
|
||||||
} else if (node.type === 'characterToken') {
|
} else if (node.type === 'characterToken') {
|
||||||
text += node.attrs.name;
|
text += node.attrs.name + ' ' + node.attrs.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,18 @@ export interface RoleEntity {
|
|||||||
isChangeRole: boolean;
|
isChangeRole: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剧本中角色实体接口
|
||||||
|
*/
|
||||||
|
export interface ScriptRoleEntity {
|
||||||
|
/** 唯一标识 */
|
||||||
|
readonly id: string;
|
||||||
|
/** 角色名称 */
|
||||||
|
name: string;
|
||||||
|
/** 角色照片 */
|
||||||
|
image_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 场景实体接口
|
* 场景实体接口
|
||||||
*/
|
*/
|
||||||
@ -76,7 +88,7 @@ export interface VideoSegmentEntity {
|
|||||||
video_status: number | null;
|
video_status: number | null;
|
||||||
}[];
|
}[];
|
||||||
/**视频片段状态 0:视频加载中 1:任务已完成 2:任务失败 */
|
/**视频片段状态 0:视频加载中 1:任务已完成 2:任务失败 */
|
||||||
status: number;
|
status: number|null;
|
||||||
/**镜头项 */
|
/**镜头项 */
|
||||||
lens: LensType[];
|
lens: LensType[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { VideoFlowProjectResponse } from "@/api/DTO/movieEdit";
|
import { VideoFlowProjectResponse } from "@/api/DTO/movieEdit";
|
||||||
import { task_item, VideoSegmentEntityAdapter } from "../adapter/oldErrAdapter";
|
import { task_item, VideoSegmentEntityAdapter } from "../adapter/oldErrAdapter";
|
||||||
import { VideoSegmentEntity } from "../domain/Entities";
|
import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities";
|
||||||
import { LensType } from "../domain/valueObject";
|
import { LensType } from "../domain/valueObject";
|
||||||
import {
|
import {
|
||||||
getShotList,
|
getShotList,
|
||||||
@ -24,7 +24,7 @@ export class VideoSegmentEditUseCase {
|
|||||||
* @param projectId 项目ID
|
* @param projectId 项目ID
|
||||||
* @returns Promise<VideoSegmentEntity[]> 视频片段列表
|
* @returns Promise<VideoSegmentEntity[]> 视频片段列表
|
||||||
*/
|
*/
|
||||||
async getVideoSegmentList(projectId: string,callback?:()=>void): Promise<VideoSegmentEntity[]> {
|
async getVideoSegmentList(projectId: string,callback?:()=>void): Promise<{ segments: VideoSegmentEntity[], roles: ScriptRoleEntity[] }> {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
@ -35,7 +35,6 @@ export class VideoSegmentEditUseCase {
|
|||||||
}
|
}
|
||||||
if(response.data.task_status=="COMPLETED"){
|
if(response.data.task_status=="COMPLETED"){
|
||||||
callback?.();
|
callback?.();
|
||||||
|
|
||||||
}
|
}
|
||||||
const Segments = VideoSegmentEntityAdapter.toVideoSegmentEntity(response.data) || [];
|
const Segments = VideoSegmentEntityAdapter.toVideoSegmentEntity(response.data) || [];
|
||||||
const detail = await detailScriptEpisodeNew({ project_id: projectId });
|
const detail = await detailScriptEpisodeNew({ project_id: projectId });
|
||||||
@ -43,7 +42,10 @@ export class VideoSegmentEditUseCase {
|
|||||||
throw new Error(detail.message || "获取视频片段列表失败");
|
throw new Error(detail.message || "获取视频片段列表失败");
|
||||||
}
|
}
|
||||||
// 匹配视频片段ID
|
// 匹配视频片段ID
|
||||||
return this.matchVideoSegmentsWithIds(Segments, detail.data);
|
return {
|
||||||
|
segments: this.matchVideoSegmentsWithIds(Segments, detail.data),
|
||||||
|
roles: response.data.project_characters
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取视频片段列表失败:", error);
|
console.error("获取视频片段列表失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -265,7 +267,7 @@ export class VideoSegmentEditUseCase {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
// 获取当前项目的视频片段列表
|
// 获取当前项目的视频片段列表
|
||||||
const segments = await this.getVideoSegmentList(projectId);
|
const { segments } = await this.getVideoSegmentList(projectId);
|
||||||
|
|
||||||
// 过滤出除当前选中片段外的所有片段
|
// 过滤出除当前选中片段外的所有片段
|
||||||
const otherSegments = segments.filter(segment => segment.id !== currentSegmentId);
|
const otherSegments = segments.filter(segment => segment.id !== currentSegmentId);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
videoSegments,
|
videoSegments,
|
||||||
|
scriptRoles,
|
||||||
getVideoSegmentList,
|
getVideoSegmentList,
|
||||||
setSelectedSegment,
|
setSelectedSegment,
|
||||||
regenerateVideoSegment,
|
regenerateVideoSegment,
|
||||||
@ -104,9 +105,10 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
|||||||
}, [scriptBlocksMemo]);
|
}, [scriptBlocksMemo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('useEditData-----videoSegments', videoSegments);
|
console.log('useEditData-----videoSegments', videoSegments, scriptRoles);
|
||||||
setShotData(videoSegments);
|
setShotData(videoSegments);
|
||||||
}, [videoSegments]);
|
setRoleData(scriptRoles);
|
||||||
|
}, [videoSegments, scriptRoles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRoleData(roleList);
|
setRoleData(roleList);
|
||||||
@ -121,6 +123,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
|||||||
applyScript,
|
applyScript,
|
||||||
// shot
|
// shot
|
||||||
shotData,
|
shotData,
|
||||||
|
scriptRoles,
|
||||||
setSelectedSegment,
|
setSelectedSegment,
|
||||||
regenerateVideoSegment,
|
regenerateVideoSegment,
|
||||||
filterRole,
|
filterRole,
|
||||||
|
|||||||
@ -165,8 +165,8 @@ CharacterTabContentProps
|
|||||||
// 处理替换确认逻辑
|
// 处理替换确认逻辑
|
||||||
console.log('Selected shots:', selectedShots);
|
console.log('Selected shots:', selectedShots);
|
||||||
console.log('Add to library:', addToLibrary);
|
console.log('Add to library:', addToLibrary);
|
||||||
await applyRoleToSelectedShots(selectedRole || {} as RoleEntity);
|
|
||||||
setIsReplacePanelOpen(false);
|
setIsReplacePanelOpen(false);
|
||||||
|
await applyRoleToSelectedShots(selectedRole || {} as RoleEntity);
|
||||||
if(addToLibrary){
|
if(addToLibrary){
|
||||||
await saveRoleToLibrary();
|
await saveRoleToLibrary();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export function EditModal({
|
|||||||
const characterTabContentRef = useRef<any>(null);
|
const characterTabContentRef = useRef<any>(null);
|
||||||
// 添加一个状态来标记是否是从切换tab触发的提醒
|
// 添加一个状态来标记是否是从切换tab触发的提醒
|
||||||
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
|
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
|
||||||
|
const [disabledBtn, setDisabledBtn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentIndex(currentSketchIndex);
|
setCurrentIndex(currentSketchIndex);
|
||||||
@ -129,6 +130,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmGotoFallback = () => {
|
const handleConfirmGotoFallback = () => {
|
||||||
|
setDisabledBtn(true);
|
||||||
console.log('handleConfirmGotoFallback');
|
console.log('handleConfirmGotoFallback');
|
||||||
SaveEditUseCase.saveData();
|
SaveEditUseCase.saveData();
|
||||||
if (activeTab === '0') {
|
if (activeTab === '0') {
|
||||||
@ -140,6 +142,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
|
|||||||
setIsRemindFallbackOpen(false);
|
setIsRemindFallbackOpen(false);
|
||||||
// 关闭弹窗
|
// 关闭弹窗
|
||||||
onClose();
|
onClose();
|
||||||
|
setDisabledBtn(false);
|
||||||
}
|
}
|
||||||
const handleCloseRemindFallbackPanel = () => {
|
const handleCloseRemindFallbackPanel = () => {
|
||||||
if (pendingSwitchTabId) {
|
if (pendingSwitchTabId) {
|
||||||
@ -316,18 +319,20 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
|
|||||||
<div className="p-4 border-t border-white/10 bg-black/20">
|
<div className="p-4 border-t border-white/10 bg-black/20">
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<motion.button
|
<motion.button
|
||||||
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
|
disabled={disabledBtn}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => {handleSave()}}
|
onClick={() => {handleSave()}}
|
||||||
|
disabled={disabledBtn}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react';
|
import { Check, X, CircleAlert, ArrowLeft, ArrowRight, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
interface ReplacePanelProps {
|
interface ReplacePanelProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -29,33 +30,12 @@ export function ReplacePanel({
|
|||||||
);
|
);
|
||||||
const [addToLibrary, setAddToLibrary] = useState(false);
|
const [addToLibrary, setAddToLibrary] = useState(false);
|
||||||
const [hoveredVideoId, setHoveredVideoId] = useState<string | null>(null);
|
const [hoveredVideoId, setHoveredVideoId] = useState<string | null>(null);
|
||||||
const [isAtStart, setIsAtStart] = useState(true);
|
|
||||||
const [isAtEnd, setIsAtEnd] = useState(false);
|
|
||||||
const videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({});
|
const videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({});
|
||||||
const shotsRef = useRef<HTMLDivElement>(null);
|
const shotsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 检查滚动位置
|
useEffect(() => {
|
||||||
const checkScrollPosition = () => {
|
console.log('replace-panel-shots', shots);
|
||||||
if (!shotsRef.current) return;
|
}, [shots]);
|
||||||
|
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = shotsRef.current;
|
|
||||||
setIsAtStart(scrollLeft <= 0);
|
|
||||||
setIsAtEnd(Math.ceil(scrollLeft + clientWidth) >= scrollWidth);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加滚动事件监听
|
|
||||||
React.useEffect(() => {
|
|
||||||
const shotsElement = shotsRef.current;
|
|
||||||
if (!shotsElement) return;
|
|
||||||
|
|
||||||
shotsElement.addEventListener('scroll', checkScrollPosition);
|
|
||||||
// 初始检查
|
|
||||||
checkScrollPosition();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
shotsElement.removeEventListener('scroll', checkScrollPosition);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShotToggle = (shotId: string) => {
|
const handleShotToggle = (shotId: string) => {
|
||||||
// setSelectedShots(prev =>
|
// setSelectedShots(prev =>
|
||||||
@ -65,10 +45,6 @@ export function ReplacePanel({
|
|||||||
// );
|
// );
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAllShots = (checked: boolean) => {
|
|
||||||
setSelectedShots(checked ? shots.map(shot => shot.id) : []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseEnter = (shotId: string) => {
|
const handleMouseEnter = (shotId: string) => {
|
||||||
setHoveredVideoId(shotId);
|
setHoveredVideoId(shotId);
|
||||||
if (videoRefs.current[shotId]) {
|
if (videoRefs.current[shotId]) {
|
||||||
@ -84,8 +60,22 @@ export function ReplacePanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const throttledConfirm = React.useCallback(
|
||||||
|
throttle(() => {
|
||||||
onConfirm(selectedShots, addToLibrary);
|
onConfirm(selectedShots, addToLibrary);
|
||||||
|
}, 1000, { trailing: false }), // 1秒内只能触发一次,不要执行最后一次调用
|
||||||
|
[selectedShots, addToLibrary, onConfirm]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 在组件卸载时取消节流函数中的定时器
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
throttledConfirm.cancel();
|
||||||
|
};
|
||||||
|
}, [throttledConfirm]);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
throttledConfirm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLeftArrowClick = () => {
|
const handleLeftArrowClick = () => {
|
||||||
@ -158,7 +148,22 @@ export function ReplacePanel({
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
{shot.videoUrl && shot.videoUrl.length > 0 && (
|
<>
|
||||||
|
{(shot.status === 0 || !shot.videoUrl.length) && (
|
||||||
|
<div className="w-full h-full absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||||
|
<div className="text-white text-sm">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shot.status === 2 && (
|
||||||
|
<div className="w-full h-full absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||||
|
<div className="text-white text-sm">
|
||||||
|
<CircleAlert className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shot.status === 1 && shot.videoUrl.length && (
|
||||||
<video
|
<video
|
||||||
ref={el => {
|
ref={el => {
|
||||||
if (el) videoRefs.current[shot.id] = el;
|
if (el) videoRefs.current[shot.id] = el;
|
||||||
@ -170,17 +175,7 @@ export function ReplacePanel({
|
|||||||
playsInline
|
playsInline
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(!shot.videoUrl || shot.videoUrl.length === 0) && (
|
|
||||||
<>
|
|
||||||
<img
|
|
||||||
src={shot.sketchUrl}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
|
||||||
<div className="text-white text-sm">Generating...</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -189,18 +184,18 @@ export function ReplacePanel({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-1/2 -translate-y-1/2 left-0 w-10 h-10 rounded-full bg-black/50 flex items-center justify-center transition-opacity duration-200",
|
"absolute top-1/2 -translate-y-1/2 left-0 w-10 h-10 rounded-full bg-black/50 flex items-center justify-center transition-opacity duration-200",
|
||||||
isAtStart ? "opacity-30 cursor-not-allowed" : "opacity-100 cursor-pointer hover:bg-black/70"
|
"opacity-100 cursor-pointer hover:bg-black/70"
|
||||||
)}
|
)}
|
||||||
onClick={() => !isAtStart && handleLeftArrowClick()}
|
onClick={() => handleLeftArrowClick()}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4 text-white" />
|
<ArrowLeft className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-1/2 -translate-y-1/2 right-0 w-10 h-10 rounded-full bg-black/50 flex items-center justify-center transition-opacity duration-200",
|
"absolute top-1/2 -translate-y-1/2 right-0 w-10 h-10 rounded-full bg-black/50 flex items-center justify-center transition-opacity duration-200",
|
||||||
isAtEnd ? "opacity-30 cursor-not-allowed" : "opacity-100 cursor-pointer hover:bg-black/70"
|
"opacity-100 cursor-pointer hover:bg-black/70"
|
||||||
)}
|
)}
|
||||||
onClick={() => !isAtEnd && handleRightArrowClick()}
|
onClick={() => handleRightArrowClick()}
|
||||||
>
|
>
|
||||||
<ArrowRight className="w-4 h-4 text-white" />
|
<ArrowRight className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,28 +3,16 @@ import { ReactNodeViewRenderer, NodeViewWrapper, ReactNodeViewProps } from '@tip
|
|||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { Check, CircleUserRound } from 'lucide-react'
|
import { Check, CircleUserRound } from 'lucide-react'
|
||||||
|
import { ScriptRoleEntity } from '@/app/service/domain/Entities';
|
||||||
interface CharacterAttributes {
|
|
||||||
id: string | null;
|
|
||||||
name: string;
|
|
||||||
avatar: string;
|
|
||||||
gender: string;
|
|
||||||
age: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Role {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CharacterTokenOptions {
|
interface CharacterTokenOptions {
|
||||||
roles?: Role[];
|
roles?: ScriptRoleEntity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CharacterToken(props: ReactNodeViewProps) {
|
export function CharacterToken(props: ReactNodeViewProps) {
|
||||||
const [showRoleList, setShowRoleList] = useState(false);
|
const [showRoleList, setShowRoleList] = useState(false);
|
||||||
const [listPosition, setListPosition] = useState({ top: 0, left: 0 });
|
const [listPosition, setListPosition] = useState({ top: 0, left: 0 });
|
||||||
const { name, avatar } = props.node.attrs as CharacterAttributes;
|
const { name } = props.node.attrs as ScriptRoleEntity;
|
||||||
const extension = props.extension as Node<CharacterTokenOptions>;
|
const extension = props.extension as Node<CharacterTokenOptions>;
|
||||||
const roles = extension.options.roles || [];
|
const roles = extension.options.roles || [];
|
||||||
|
|
||||||
@ -76,7 +64,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
}
|
}
|
||||||
}, [showRoleList]);
|
}, [showRoleList]);
|
||||||
|
|
||||||
const handleRoleSelect = (role: Role) => {
|
const handleRoleSelect = (role: ScriptRoleEntity) => {
|
||||||
const { editor } = props;
|
const { editor } = props;
|
||||||
const pos = props.getPos();
|
const pos = props.getPos();
|
||||||
|
|
||||||
@ -84,8 +72,9 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
const { tr } = editor.state;
|
const { tr } = editor.state;
|
||||||
tr.setNodeMarkup(pos, undefined, {
|
tr.setNodeMarkup(pos, undefined, {
|
||||||
...props.node.attrs,
|
...props.node.attrs,
|
||||||
|
id: role.id,
|
||||||
name: role.name,
|
name: role.name,
|
||||||
avatar: role.url,
|
image_url: role.image_url,
|
||||||
});
|
});
|
||||||
editor.view.dispatch(tr);
|
editor.view.dispatch(tr);
|
||||||
}
|
}
|
||||||
@ -137,7 +126,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
src={role.url}
|
src={role.image_url}
|
||||||
alt={role.name}
|
alt={role.name}
|
||||||
className={`w-10 h-10 rounded-full border transition-all duration-200
|
className={`w-10 h-10 rounded-full border transition-all duration-200
|
||||||
${isSelected ? 'border-blue-400 border-2' : 'border-white/20'}`}
|
${isSelected ? 'border-blue-400 border-2' : 'border-white/20'}`}
|
||||||
@ -158,7 +147,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
data-alt="role-item"
|
data-alt="role-item"
|
||||||
className={`flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors duration-200
|
className={`flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors duration-200
|
||||||
${name === 'Voiceover' ? 'bg-blue-500/20 text-blue-400' : 'hover:bg-white/5 text-gray-200'}`}
|
${name === 'Voiceover' ? 'bg-blue-500/20 text-blue-400' : 'hover:bg-white/5 text-gray-200'}`}
|
||||||
onClick={() => handleRoleSelect({ name: 'Voiceover', url: '' })}
|
onClick={() => handleRoleSelect({ name: 'Voiceover', image_url: '', id: 'voiceover' } as ScriptRoleEntity)}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CircleUserRound
|
<CircleUserRound
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
shotData,
|
shotData,
|
||||||
|
scriptRoles,
|
||||||
setSelectedSegment,
|
setSelectedSegment,
|
||||||
regenerateVideoSegment,
|
regenerateVideoSegment,
|
||||||
filterRole,
|
filterRole,
|
||||||
@ -50,8 +51,8 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('shotTabContent-----roles', roles);
|
console.log('shotTabContent-----scriptRoles', scriptRoles);
|
||||||
}, [roles]);
|
}, [scriptRoles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingRegeneration) {
|
if (pendingRegeneration) {
|
||||||
@ -243,7 +244,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shot.status === 1 && (
|
{shot.status === 1 && shot.videoUrl[0] ? (
|
||||||
<video
|
<video
|
||||||
src={shot.videoUrl[0].video_url}
|
src={shot.videoUrl[0].video_url}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
@ -253,6 +254,10 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
onMouseEnter={(e) => e.currentTarget.play()}
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
|
||||||
|
<CircleX className="w-4 h-4 text-red-500" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 任务失败 */}
|
{/* 任务失败 */}
|
||||||
{shot.status === 2 && (
|
{shot.status === 2 && (
|
||||||
@ -395,7 +400,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
|||||||
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
||||||
<ShotsEditor
|
<ShotsEditor
|
||||||
ref={shotsEditorRef}
|
ref={shotsEditorRef}
|
||||||
roles={roles}
|
roles={scriptRoles}
|
||||||
shotInfo={shotData[selectedIndex].lens}
|
shotInfo={shotData[selectedIndex].lens}
|
||||||
style={{height: 'calc(100% - 4rem)'}}
|
style={{height: 'calc(100% - 4rem)'}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user