更新 API 常量以使用环境变量,移除不必要的日志输出,重构剧本服务以支持新的项目创建和剧本保存逻辑,增加故事细节的提取和更新功能。

This commit is contained in:
海龙 2025-08-06 18:53:21 +08:00
parent 96b8066f5c
commit 073bc6db13
9 changed files with 437 additions and 86 deletions

View File

@ -1,2 +1,2 @@
export const BASE_URL = "https://77.smartvideo.py.qikongjian.com"
// process.env.NEXT_PUBLIC_API_BASE_URL
export const BASE_URL = process.env.NEXT_PUBLIC_SMART_API
//

View File

@ -28,8 +28,6 @@ request.interceptors.request.use(
request.interceptors.response.use(
(response: AxiosResponse) => {
// 直接返回响应数据
console.log('?????????????????????????',Object.keys(response.data));
return response.data;
},
(error) => {

View File

@ -599,10 +599,12 @@ export const generateScriptStream = (
case 'completed':
console.log('生成完成:', data.message);
resolve()
return;
case 'error':
console.error('生成失败:', data.message);
reject(data.message)
return;
}
})
@ -616,11 +618,11 @@ export const generateScriptStream = (
*/
export const applyScriptToShot = async (request: {
/** 项目ID */
projectId: string;
/** 剧本*/
scriptText: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/movie/apply_script_to_shot", request);
project_id: string;
/** 计划Id*/
plan_id: string;
})=> {
return post<ApiResponse<any>>("/movie/create_movie_project_plan_v1", request);
};
/**
@ -629,19 +631,14 @@ export const applyScriptToShot = async (request: {
*/
export const getProjectScript = async (request: {
/** 项目ID */
projectId: string;
}): Promise<
ApiResponse<{
/** 用户提示词 */
prompt: string;
/** 生成的剧本文本 */
scriptText: string;
}>
> => {
project_id: string;
})=> {
return post<
ApiResponse<{
prompt: string;
scriptText: string;
/** 项目id */
project_id: string;
/** 生成的剧本文本 */
generated_script: string;
}>
>("/movie/get_project_script", request);
};
@ -653,32 +650,54 @@ export const getProjectScript = async (request: {
*/
export const saveScript = async (request: {
/** 项目ID */
projectId: string;
project_id: string;
/** 剧本文本 */
scriptText: string;
generated_script: string;
/** 剧情梗概 */
synopsis: string;
/** 剧情类型 */
categories: string[];
/** 主角 */
protagonist: string;
/** 框架 */
framework: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/movie/save_script", request);
return post<ApiResponse<any>>("/movie/update_generated_script", request);
};
/**
*
* @param request
* V1版本
* @param request
* @returns Promise<ApiResponse<{ projectId: string }>>
*/
export const createProject = async (request: {
/** 用户提示词 */
userPrompt: string;
/** 剧本内容 */
scriptContent: string;
}): Promise<
ApiResponse<{
/** 项目ID */
projectId: string;
}>
> => {
return post<
ApiResponse<{
projectId: string;
}>
>("/movie/create_project", request);
export const abortVideoTask = async (request: {
/** 项目ID */
project_id: string;
/** 计划ID */
plan_id: string;
}): Promise<ApiResponse<any>> => {
return post("/api/v1/video/abort", request);
};
export const createMovieProjectV1 = async (request: {
/** 剧本内容 */
script: string;
/** 用户ID */
user_id: string;
/** 模式auto | manual */
mode: "auto" | "manual";
/** 分辨率720p | 1080p | 4k */
resolution: "720p" | "1080p" | "4k";
}) => {
return post<ApiResponse<{
/** 项目ID */
project_id: string;
/** 计划ID */
plan_id: string;
/** 项目名称 */
name: string;
/** 项目状态 */
status: string;
}>>("/movie/create_movie_project_v1", request);
};

View File

@ -1,6 +1,6 @@
import { useState, useCallback, useMemo } from 'react';
import { SceneEntity, TagEntity, AITextEntity, VideoSegmentEntity } from '../domain/Entities';
import { SceneItem, TagItem, TextItem, ShotItem } from '../domain/Item';
import { SceneItem, TagItem, TextItem } from '../domain/Item';
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
import { TextEditUseCase } from '../usecase/TextEditUseCase';

View File

@ -106,6 +106,230 @@ export class LensType {
}
}
/**
* DDD概念的值对象
*
* -
* - 使
*/
export class StoryDetails {
/** 故事梗概 */
public synopsis: string = "";
/** 故事分类标签 */
public categories: string[] = [];
/** 主角名称 */
public protagonist: string = "";
/** 激励事件 */
public incitingIncident: string = "";
/** 问题与新目标 */
public problem: string = "";
/** 冲突与障碍 */
public conflict: string = "";
/** 赌注 */
public stakes: string = "";
/** 人物弧线完成 */
public characterArc: string = "";
/**
*
* @param text
*/
updateScript(text: string) {
const scriptObject = this.createFromText(text);
this.synopsis = scriptObject.synopsis;
this.categories = scriptObject.categories;
this.protagonist = scriptObject.protagonist;
this.incitingIncident = scriptObject.incitingIncident;
this.problem = scriptObject.problem;
this.conflict = scriptObject.conflict;
this.stakes = scriptObject.stakes;
this.characterArc = scriptObject.characterArc;
return scriptObject;
}
mergeframework():string{
return `
${this.incitingIncident}\n
${this.problem}
${this.conflict}
${this.stakes}
${this.characterArc}
`
}
constructor(text: string) {
this.updateScript(text);
}
/**
* StoryDetails
*
* @param text Markdown或富文本
*/
public createFromText(text: string) {
// --- 智能解析梗概 ---
// 梗概通常位于“Core Elements”部分描述主角和初始事件。
// 我们提取“Protagonist”和“The Inciting Incident”的描述并组合起来。
const protagonistText = this.extractContentByHeader(text, "Core Identity:");
const incitingIncidentText = this.extractContentByHeader(
text,
"The Inciting Incident"
);
// --- 其他字段的提取保持不变,使用标题进行查找 ---
const categories = this.extractContentByHeader(text, "GENRE")
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
const protagonist = this.extractContentByHeader(text, "Core Identity:");
const problem = this.extractContentByHeader(text, "The Problem & New Goal");
const conflict = this.extractContentByHeader(text, "Conflict & Obstacles");
const stakes = this.extractContentByHeader(text, "The Stakes");
const characterArc = this.extractContentByHeader(
text,
"Character Arc Accomplished"
);
// 1. 梗概 - 从主角描述和激励事件组合生成
const synopsis = `${protagonistText} ${incitingIncidentText}`.trim();
return {
synopsis,
categories,
protagonist,
incitingIncident: incitingIncidentText,
problem,
conflict,
stakes,
characterArc
};
}
/**
* Markdown标签
* @param fullText
* @param headerName
* @returns
*/
public extractContentByHeader(fullText: string, headerName: string): string {
try {
console.log(`正在查找标题: "${headerName}"`);
// 转义正则表达式中的特殊字符
const escapeRegex = (text: string): string => {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
// 清理Markdown格式和多余的空白字符
const cleanMarkdownContent = (content: string): string => {
return content
// 移除markdown的粗体标记
.replace(/\*\*([^*]+)\*\*/g, '$1')
// 移除markdown的斜体标记
.replace(/\*([^*]+)\*/g, '$1')
// 移除列表标记(- 或 * 开头的行)
.replace(/^\s*[-*]\s+/gm, '')
// 移除代码块
.replace(/```[\s\S]*?```/g, '')
// 移除行内代码
.replace(/`([^`]+)`/g, '$1')
// 规范化空白字符:将多个连续空白(包括换行)替换为单个空格
.replace(/\s+/g, ' ')
// 移除引号
.replace(/["""'']/g, '')
// 最终清理首尾空白
.trim();
};
// 标准化headerName移除末尾的冒号和空格
const normalizedHeaderName = headerName.replace(/:?\s*$/, '').trim();
const escapedHeaderName = escapeRegex(normalizedHeaderName);
let content = "";
// 定义多种匹配模式,按优先级排序
const patterns = [
// 1. 匹配带编号的主标题:数字. **标题:** 格式
new RegExp(`\\d+\\.\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"),
// 2. 匹配子标题:*标题:* 格式(在主标题下的子项)
new RegExp(`\\*\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\*\\*[A-Z]+:\\*\\*|---|\$)`, "i"),
// 3. 匹配独立的粗体标题:**标题:** 格式
new RegExp(`^\\s*\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=^\\s*\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|^\\s*\\*\\*[A-Z]+:\\*\\*|^---|\$)`, "im"),
// 4. 匹配简单的**标题:**格式(可能在段落中)
new RegExp(`\\*\\*${escapedHeaderName}:?\\*\\*\\s*([\\s\\S]*?)(?=\\*\\*[^*]+:?\\*\\*|\\d+\\.\\s*\\*\\*[^*]+:?\\*\\*|\\n\\n|---|\$)`, "i"),
// 5. 匹配markdown标题## 标题 格式
new RegExp(`^#{1,6}\\s*${escapedHeaderName}:?\\s*\\n([\\s\\S]*?)(?=^#{1,6}\\s|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"),
// 6. 匹配冒号后的内容:标题: 内容(适用于简单的键值对格式)
new RegExp(`^\\s*${escapedHeaderName}:?\\s*([\\s\\S]*?)(?=^\\s*[A-Za-z][^:]*:|^\\*\\*[^*]+:?\\*\\*|^---|\$)`, "im"),
];
// 尝试每种模式
for (let i = 0; i < patterns.length; i++) {
const regex = patterns[i];
const match = fullText.match(regex);
if (match && match[1] && match[1].trim()) {
content = match[1].trim();
console.log(`使用模式 ${i + 1} 匹配成功`);
console.log(`匹配的完整内容: "${match[0].substring(0, 150)}..."`);
break;
}
}
if (!content) {
console.log(`没有找到标题 "${headerName}" 对应的内容`);
// 尝试更宽松的匹配作为最后手段
const loosePattern = new RegExp(`${escapedHeaderName}[:\\s]*([\\s\\S]{1,500}?)(?=\\n\\n|\\*\\*[^*]+\\*\\*|#{1,6}\\s|---|\$)`, "i");
const looseMatch = fullText.match(loosePattern);
if (looseMatch && looseMatch[1]) {
content = looseMatch[1].trim();
console.log("使用宽松匹配模式找到内容");
} else {
return "";
}
}
console.log(`原始匹配内容: "${content.substring(0, 100)}..."`);
// 清理内容格式
content = cleanMarkdownContent(content);
// 如果内容太短,可能是匹配错误,返回空
if (content.length < 10) {
console.log("匹配到的内容太短,可能匹配错误");
return "";
}
console.log(`清理后的内容: "${content.substring(0, 100)}..."`);
return content;
} catch (error) {
console.error("内容提取出错:", error);
return "";
}
}
toObject() {
return {
synopsis: this.synopsis,
categories: this.categories,
protagonist: this.protagonist,
incitingIncident: this.incitingIncident,
problem: this.problem,
conflict: this.conflict,
stakes: this.stakes,
characterArc: this.characterArc,
};
}
}
/**
*
* @description ScriptSlice的管理与行为
@ -120,12 +344,14 @@ export class ScriptValueObject {
get scriptSlices(): readonly ScriptSlice[] {
return [...this._scriptSlices];
}
scriptText: string = "";
storyDetails: StoryDetails;
/**
* @description:
* @param scriptText
*/
constructor(scriptText?: string) {
this.storyDetails = new StoryDetails("");
if (scriptText) {
this.parseFromString(scriptText);
}
@ -135,30 +361,15 @@ export class ScriptValueObject {
* @description:
* @param scriptText
*/
parseFromString(scriptText: string): void {
console.log('scriptText', scriptText)
parseFromString(scriptText: string) {
this.scriptText += scriptText;
return this.storyDetails.updateScript(this.scriptText);
}
/**
* @description:
* @returns string
*/
toString(): string {
return this._scriptSlices.map((slice) => slice.text).join("");
}
/**
*
* @param other ScriptValueObject实例
* @returns
*/
equals(other: ScriptValueObject): boolean {
if (this._scriptSlices.length !== other._scriptSlices.length) {
return false;
}
return this._scriptSlices.every((slice, index) =>
slice.equals(other._scriptSlices[index])
);
return this.scriptText;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import { ScriptSlice, ScriptValueObject } from "../domain/valueObject";
import { generateScriptStream, applyScriptToShot } from "@/api/video_flow";
import { generateScriptStream, applyScriptToShot, createMovieProjectV1, saveScript } from "@/api/video_flow";
export class ScriptEditUseCase {
loading: boolean = false;
@ -30,6 +30,8 @@ export class ScriptEditUseCase {
stream_callback?.(content)
this.scriptValueObject.parseFromString(content)
});
} catch (error) {
if (this.abortController?.signal.aborted) {
console.log("剧本生成被中断");
@ -53,18 +55,84 @@ export class ScriptEditUseCase {
}
}
/**
* @description:
* @param prompt
* @param userId ID
* @param mode auto | manual
* @param resolution 720p | 1080p | 4k
* @returns Promise<string> ID
*/
async createProject(
prompt: string,
userId: string = "user123",
mode: "auto" | "manual" = "auto",
resolution: "720p" | "1080p" | "4k" = "720p"
) {
try {
// 调用创建项目API
const response = await createMovieProjectV1({
script: prompt,
user_id: userId,
mode,
resolution
});
if (!response.successful) {
throw new Error(response.message || "创建项目失败");
}
return response.data;
} catch (error) {
console.error("创建项目失败:", error);
throw error;
}
}
/**
* @description:
* @param projectId ID
* @returns Promise<void>
*/
async saveScript(projectId: string): Promise<void> {
try {
this.loading = true;
// 获取当前剧本文本
const scriptText = this.scriptValueObject.toString();
// 调用保存剧本API
const response = await saveScript({
project_id: projectId,
generated_script: scriptText,
synopsis: this.scriptValueObject.storyDetails.synopsis,
categories: this.scriptValueObject.storyDetails.categories,
protagonist: this.scriptValueObject.storyDetails.protagonist,
framework: this.scriptValueObject.storyDetails.mergeframework()
});
if (!response.successful) {
throw new Error(response.message || "保存剧本失败");
}
return response.data;
} catch (error) {
console.error("保存剧本失败:", error);
throw error;
} finally {
this.loading = false;
}
}
/**
* @description:
* @returns Promise<void>
*/
async applyScript(projectId: string): Promise<void> {
async applyScript(projectId: string,planId: string): Promise<void> {
try {
this.loading = true;
// 调用应用剧本接口
const response = await applyScriptToShot({
projectId,
scriptText: this.scriptValueObject.toString(),
project_id: projectId,
plan_id: planId
});
if (!response.successful) {
@ -87,7 +155,13 @@ export class ScriptEditUseCase {
getScriptSlices(): ScriptSlice[] {
return [...this.scriptValueObject.scriptSlices];
}
/**
* @description:
* @returns StoryDetails
*/
getStoryDetails() {
return this.scriptValueObject.storyDetails.toObject();
}
/**
* @description:
* @returns boolean

View File

@ -1,7 +1,7 @@
import { BASE_URL } from "@/api/constants";
// API配置
const API_BASE_URL = 'https://77.api.qikongjian.com';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || '';
// Token存储键
const TOKEN_KEY = 'X-EASE-ADMIN-TOKEN';
@ -29,7 +29,7 @@ export const loginUser = async (email: string, password: string) => {
if (data.code === '200' && data.status === 200) {
// 保存token到本地存储
setToken(data.body.token);
// 保存用户信息
const user = {
id: data.body.userId,
@ -38,7 +38,7 @@ export const loginUser = async (email: string, password: string) => {
token: data.body.token,
};
setUser(user);
return user;
} else {
throw new Error(data.msg || '登录失败');
@ -104,10 +104,10 @@ export const setToken = (token: string) => {
*/
export const getCurrentUser = () => {
if (typeof window === 'undefined') return null;
const userJson = localStorage.getItem(USER_KEY);
if (!userJson) return null;
try {
return JSON.parse(userJson);
} catch (error) {
@ -165,7 +165,7 @@ export const getAuthHeaders = () => {
*/
export const authFetch = async (url: string, options: RequestInit = {}) => {
const token = getToken();
if (!token) {
throw new Error('No token available');
}
@ -198,7 +198,7 @@ export const authFetch = async (url: string, options: RequestInit = {}) => {
// Google OAuth相关函数保持不变
const GOOGLE_CLIENT_ID = '1016208801816-qtvcvki2jobmcin1g4e7u4sotr0p8g3u.apps.googleusercontent.com';
const GOOGLE_REDIRECT_URI = typeof window !== 'undefined'
const GOOGLE_REDIRECT_URI = typeof window !== 'undefined'
? BASE_URL+'/users/oauth/callback'
: '';
@ -207,7 +207,7 @@ const GOOGLE_REDIRECT_URI = typeof window !== 'undefined'
*/
export const signInWithGoogle = () => {
const state = generateOAuthState();
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: GOOGLE_REDIRECT_URI,
@ -226,13 +226,13 @@ export const signInWithGoogle = () => {
*/
export const generateOAuthState = () => {
if (typeof window === 'undefined') return '';
// Generate a random string for state
const state = Math.random().toString(36).substring(2, 15);
// Store the state in session storage to validate later
sessionStorage.setItem('oauthState', state);
return state;
};
@ -241,12 +241,12 @@ export const generateOAuthState = () => {
*/
export const validateOAuthState = (state: string): boolean => {
if (typeof window === 'undefined') return false;
const storedState = sessionStorage.getItem('oauthState');
// Clean up the stored state regardless of validity
sessionStorage.removeItem('oauthState');
// Validate that the returned state matches what we stored
return state === storedState;
};
};