diff --git a/api/constants.ts b/api/constants.ts index eb4fbba..53e1866 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -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 +// diff --git a/api/request.ts b/api/request.ts index 36fc35d..421a929 100644 --- a/api/request.ts +++ b/api/request.ts @@ -28,8 +28,6 @@ request.interceptors.request.use( request.interceptors.response.use( (response: AxiosResponse) => { // 直接返回响应数据 - console.log('?????????????????????????',Object.keys(response.data)); - return response.data; }, (error) => { diff --git a/api/video_flow.ts b/api/video_flow.ts index 465cce2..3a2a0d4 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -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> => { - return post>("/movie/apply_script_to_shot", request); + project_id: string; + /** 计划Id*/ + plan_id: string; +})=> { + return post>("/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> => { - return post>("/movie/save_script", request); + return post>("/movie/update_generated_script", request); }; + /** - * 创建项目 - * @param request 创建项目请求参数 + * 创建电影项目V1版本 + * @param request 创建电影项目请求参数 * @returns Promise> */ -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> => { + 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>("/movie/create_movie_project_v1", request); }; diff --git a/app/service/Interaction/SceneService.ts b/app/service/Interaction/SceneService.ts index 9467f1b..134feee 100644 --- a/app/service/Interaction/SceneService.ts +++ b/app/service/Interaction/SceneService.ts @@ -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'; diff --git a/app/service/domain/valueObject.ts b/app/service/domain/valueObject.ts index ee3cff7..3b93168 100644 --- a/app/service/domain/valueObject.ts +++ b/app/service/domain/valueObject.ts @@ -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; } } diff --git a/app/service/test/Script.test.ts b/app/service/test/Script.test.ts index a4188f2..c764631 100644 --- a/app/service/test/Script.test.ts +++ b/app/service/test/Script.test.ts @@ -13,15 +13,63 @@ jest.mock('../../../api/constants', () => ({ BASE_URL: 'http://127.0.0.1:8000' })); +import { StoryDetails } from "../domain/valueObject"; import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase"; describe('ScriptService 业务逻辑测试', () => { // 创建新的剧本编辑用例 const newScriptEditUseCase = new ScriptEditUseCase(''); + const script = "在阳光明媚的码头上,两只柴犬展开了一场薯条吃比赛。一只优雅的母猫担任裁判,端坐高处,威严地监督比赛。两只鸽子站在一旁,歪着头有趣地看着,偶尔咕咕低鸣。柴犬们瞪大眼睛,尾巴摇得飞快,争抢盘子里的金黄薯条。从日出到天黑,它们吃个不停,薯条堆成了小山,母猫无奈摇头,鸽子仍兴致勃勃,场面热闹非凡。"; + + let projectId: string; + let planId: string; + // let name: string; + it("想法生成剧本", async () => { - const res = await newScriptEditUseCase.generateScript("我想拍一个关于爱情的故事",(content)=>{ - console.log(content); - }); - }, 300000); // 30秒超时 + const res = await newScriptEditUseCase.generateScript(script,(content)=>{ + // console.log(content); + console.log(newScriptEditUseCase.getStoryDetails()); + }); + expect(newScriptEditUseCase.toString()).toBeDefined(); + + }, 300000); + it("剧本解析", async () => { + const scriptText = `**Core Elements**\n\n1. **Protagonist:**\n * **Core Identity:** AKIO, a young, spirited male Shiba Inu. His fur is a perfect toasted sesame color. He is naive, driven by simple impulses, and possesses an almost comical level of competitive spirit. He lives a comfortable life near a bustling seaside pier in the present day.\n * **Initial State & Flaw:** Akio begins the story content and carefree, enjoying the sun. His critical flaw is a combination of gluttony and a naive obsession with winning. He believes victory, in any form, is the ultimate measure of happiness and worth, failing to see the emptiness of a prize won without purpose.\n\n2. **The Inciting Incident:**\n * A large, forgotten plate of golden french fries is left on a low crate on the pier. His pier rival, HANA, a sleek red Shiba, spots it at the same moment. An unspoken challenge flashes between them. QUEENIE, an elegant Siamese cat who rules the pier, leaps atop a tall piling, implicitly agreeing to officiate. The contest is suddenly, irrevocably on. This event shatters Akio's peaceful morning and directly triggers his competitive flaw.\n\n3. **The Problem & New Goal:**\n * The problem is brutally simple: How can I eat more fries than Hana and win this contest? This gives Akio a new, all-consuming goal: to achieve victory and be crowned the undisputed champion of the fries.\n\n4. **Conflict & Obstacles:**\n * **Primary Conflict:** Akio vs. Hana in a direct, head-to-head eating competition. Internally, Akio battles his growing physical misery against his desperate, ingrained need to win.\n * **Key Obstacles:**\n 1. Hana is an equally matched and fiercely determined opponent.\n 2. The sheer volume of fries turns the contest from a sprint into a grueling marathon, pushing them both to their physical limits.\n\n5. **The Stakes:**\n * **What is at stake?:** If Akio wins, he gains temporary glory and the satisfaction of his ego. If he fails, he loses face to his rival. However, the true stake is what he stands to permanently lose if he *succeeds* through pure gluttony: he will lose the simple joy of food, his physical well-being, and the potential for a genuine connection with Hana, leaving him bloated and alone with a hollow victory.\n\n6. **Character Arc Accomplished:**\n * By the end, faced with the final fry, Akio overcomes his flaw. He sees the contest's absurdity and Hana's shared suffering. He chooses connection over competition, transforming from a mindless glutton into a creature capable of empathy and grace. He learns that sharing a moment is more fulfilling than winning a prize.\n\n**GENRE:** Sports\n\n---\n\n**SCENE 1**\n\n**[SCENE'S CORE CONFLICT]: The thrill of the challenge and the start of an epic, absurd competition.**\n\nEXT. PIER - DAY\n\n**Scene Transition:** The scene opens on a perfect, sun-drenched morning, establishing the peaceful status quo that is about to be shattered.\n\nThe air is salty and bright. Wooden planks, bleached by the sun, stretch out over gently lapping water. AKIO, a handsome Shiba Inu, basks in a patch of sun, tail curled, eyes blissfully shut.\n\nA WIDE SHOT establishes the key players in their starting positions. A few yards away, HANA, a fiery red Shiba, watches a seagull. On a high wooden piling, QUEENIE, a regal Siamese cat, grooms a paw with aristocratic disdain. On a nearby railing, two PIGEONS, GUS and GERTIE, coo softly.\n\nA careless tourist leaves a large paper plate piled high with golden, glistening FRENCH FRIES on a low wooden crate.\n\nCLOSE-UP on Akio's nose twitching. His eyes snap open. At the same instant, Hana turns. Their eyes lock. The air crackles.\n\nQueenie stops grooming. She stares down at them, then at the fries. With a deliberate, fluid motion, she sits perfectly upright, tail wrapped around her paws. The judge has taken her seat.\n\nAkio's tail begins to thump against the wood. A low, excited growl rumbles in his chest. Hana answers with a sharp, challenging yip.\n\n**AKIO (V.O.)**\nThis was it. The big one.\nGlory was on that plate.\n\nWithout another sound, they both lunge.\n\nRAPID CUTS between Akio and Hana gobbling fries. Their muzzles are a blur of motion. Golden sticks disappear. Tails wag like furious metronomes. The pigeons lean forward, heads cocked, their cooing becoming more rapid and excited.\n\n**GUS & GERTIE**\n(Excited cooing)\n(He's fast! But she's relentless!)\n\nQueenie watches, impassive. She lifts a single paw, as if about to signal a foul, then slowly lowers it. She will allow it. For now.\n\n**AKIO (V.O.)**\nShe was good. Real good.\nBut I was born for this.\n\n---\n\n**SCENE 2**\n\n**[SCENE'S CORE CONFLICT]: The escalation of the contest into an absurd, grueling marathon.**\n\nEXT. PIER - MIDDAY\n\n**Scene Transition:** A time-lapse montage shows the sun climbing higher in the sky. The shadows on the pier shrink. The pile of fries on the plate is noticeably smaller, but a new, larger pile of discarded, half-eaten fries is growing on the planks around it. The transition emphasizes the passage of time and the sheer endurance of the contest.\n\nThe midday sun beats down. The initial frenzy has subsided into a grim, rhythmic battle of attrition. Akio and Hana are no longer gobbling; they are CHEWING. Methodically. Grimly. Their faces are greasy. Their bellies are visibly distended.\n\n**AKIO (V.O.)**\nThe thrill was gone.\nNow, it was just a job.\nOne fry. Then the next.\n\nHana pauses, breathing heavily. A single fry hangs from her mouth. She looks at Akio. There is no fire in her eyes now, only exhaustion. Akio meets her gaze, his own jaw working slowly.\n\nQueenie yawns, a flash of pink mouth and sharp teeth. She looks utterly bored. She stretches one leg, then the other, before settling back into her judicial pose.\n\n**QUEENIE**\n(A low, bored meow)\n(Are they still going? Honestly.)\n\nGus and Gertie are still rapt. They hop from one foot to the other, nudging each other, offering quiet commentary.\n\n**GUS & GERTIE**\n(Murmuring coos)\n(His form is slipping. Look.)\n(She has more stamina. Classic.)\n\nAkio swallows with a gulp. He eyes the remaining pile. It still looks like a mountain. He glances at Hana. She takes a deep breath and doggedly chomps another fry.\n\n**AKIO (V.O.)**\nI couldn't quit. Not now.\nChampions don't quit. Right?\n\nHe forces another fry into his mouth. It tastes like cardboard and regret.\n\n---\n\n**SCENE 3**\n\n**[SCENE'S CORE CONFLICT]: The physical and emotional breaking point, where the cost of victory becomes clear.**\n\nEXT. PIER - SUNSET\n\n**Scene Transition:** The scene opens on an empty shot of the sun, a brilliant orange orb, touching the horizon. The water is a sheet of molten gold. The light is warm but fading, casting long, dramatic shadows. This transition marks the end of the day and the climax of the struggle.\n\nThe pier is bathed in the golden hour's glow. The scene is quiet, the earlier energy completely gone. Akio and Hana are lying on the planks, flanking the plate. They are panting, their sides heaving. The mountain of fries is gone.\n\nOnly ONE. SINGLE. FRY remains.\n\nIt sits perfectly in the center of the greasy plate, a final, golden trophy.\n\nAkio lifts his head. It feels like it weighs a thousand pounds. He looks at the fry, then at Hana. She is a mirror of his own misery. Her fur is matted with grease, her eyes are glassy. A tiny, pathetic whimper escapes her.\n\n**AKIO (V.O.)**\nWe had done it. We ate it all.\nBut there was no cheering.\nJust... this. This quiet ache.\n\nQueenie looks down, a flicker of something—pity? annoyance?—in her blue eyes. She lets out a soft, exasperated sigh.\n\n**QUEENIE**\n(A soft, tired meow)\n(Oh, for heaven's sake. Finish it.)\n\nAkio summons the last of his strength. He begins to drag himself towards the plate. Every muscle screams. This is the final push. The winning point. He can taste victory. It tastes like salt and exhaustion.\n\nHe reaches the plate, his nose inches from the final fry.\n\n---\n\n**SCENE 4**\n\n**[SCENE'S CORE CONFLICT]: The resolution through an act of grace, redefining victory as connection.**\n\nEXT. PIER - DUSK\n\n**Scene Transition:** Continuing directly from the previous scene's climax. The final rays of sunlight disappear, and the cool, soft light of dusk settles over the pier. The shift in light mirrors Akio's internal shift from the fiery heat of competition to the cool clarity of realization.\n\nAkio stares at the last fry. The golden light has faded, and the fry looks pale and unappetizing under the blue twilight.\n\nCLOSE-UP on Akio's eyes. We see the reflection of the lonely fry. Then his eyes shift, looking past it, towards Hana. She hasn't moved. She just lies there, defeated and miserable.\n\n**AKIO (V.O.)**\nAnd then I saw it.\nWinning meant she lost.\nWhat kind of prize was that?\n\nA profound change comes over Akio's face. The grim determination melts away, replaced by a soft, clear understanding.\n\nSLOW MOTION as Akio gently nudges the final fry with his nose. He doesn't eat it. He pushes it, slowly, deliberately, across the greasy plate until it stops directly in front of Hana's nose.\n\nHana's eyes flutter open. She looks at the fry, then at Akio. Confusion, then dawning surprise.\n\nAkio gives a small, tired tail wag. A real one. Not the frantic wag of competition, but a gentle wave of peace.\n\nHana looks at the fry for a long moment. Then she ignores it completely. She inches forward and gently licks Akio's nose.\n\nOn the piling, Queenie watches this. For the first time, a genuine, soft smile seems to grace her feline features. She lets out a quiet, approving purr.\n\n**QUEENIE**\n(A soft, rumbling purr)\n(Finally.)\n\nGus and Gertie coo softly, a gentle, contented sound. They bob their heads in unison, as if applauding.\n\nFADE OUT on the two Shibas, lying side-by-side in the twilight, the single, uneaten fry sitting between them like a forgotten trophy.\n\n**AKIO (V.O.)**\nWe didn't have a winner.\nWe had something better.\n\n---\n\n### Part 2: Addendum for Director & Performance\n\n**【Decoding the Directing Style】**\n\n* **Core Visual Tone:** A mock-epic \"sports documentary\" style. Use grand, sweeping shots in the beginning (like an NFL Films production) contrasted with gritty, handheld close-ups during the \"mid-game\" struggle. The color palette should shift dramatically: vibrant, saturated colors in Scene 1; a harsh, overexposed look in Scene 2; a warm, elegiac \"magic hour\" glow in Scene 3; and a cool, peaceful blue/purple palette for the resolution in Scene 4.\n* **Key Scene Treatment Suggestion:** For the climax in Scene 3, when only one fry remains, the sound design should drop out almost completely. All we hear is the labored breathing of the dogs, the gentle lapping of water, and the distant cry of a gull. The camera should be at a very low angle, making the single fry on the plate look like a monumental obelisk. The final action in Scene 4—Akio pushing the fry—should be captured in a single, unbroken take, focusing on the slow, deliberate movement and Hana's dawning reaction.\n\n**【The Core Performance Key】**\n\n* **The Character's Physicality:** Akio's physicality must arc. He starts with a bouncy, \"on the balls of his feet\" energy. In Scene 2, his movements become heavy and sluggish, his chewing laborious. By Scene 3, he should be practically immobile, every movement an immense effort. His final act of pushing the fry should be gentle and tender, a stark contrast to the frantic gobbling at the start.\n* **Subtextual Drive:** Akio's internal voiceover is the text; the subtext is in his eyes and body. His V.O. in Scene 2 says \"Champions don't quit,\" but his eyes should scream, \"I want my mommy.\" The subtext of the final scene is a silent apology and an offering of peace. When Hana licks his nose, the subtext is \"I accept. We're okay.\"\n\n**【Connection to the Zeitgeist】**\n\n* This story of a pointless, all-consuming competition for a meaningless prize serves as a gentle parable for the modern obsession with \"winning\" on social media and in hustle culture, suggesting that true fulfillment is found not in victory, but in shared humanity and connection.` + const s = new StoryDetails('') + try { + const protagonistText = s.extractContentByHeader(scriptText, "Core Identity:"); + const incitingIncidentText = s.extractContentByHeader( + scriptText, + "The Inciting Incident" + ); + // --- 其他字段的提取保持不变,使用标题进行查找 --- + const categories = s.extractContentByHeader(scriptText, "GENRE") + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + const protagonist = s.extractContentByHeader(scriptText, "Core Identity:"); + + const problem = s.extractContentByHeader(scriptText, "The Problem & New Goal"); + const conflict = s.extractContentByHeader(scriptText, "Conflict & Obstacles"); + const stakes = s.extractContentByHeader(scriptText, "The Stakes"); + } catch (error) { + console.error('测试执行出错:', error); + } + // expect(res).not.toBe(''); + }); + it("创建项目", async () => { + const createRes = await newScriptEditUseCase.createProject(script,"user123","auto","720p"); + expect(createRes.project_id).toBeDefined(); + projectId = createRes.project_id; + planId = createRes.plan_id; + }); + it("保存剧本", async () => { + const res = await newScriptEditUseCase.saveScript(projectId); + console.log(res); + }); + it("应用剧本", async () => { + await newScriptEditUseCase.applyScript(projectId,planId); + console.log(projectId); + }); }); diff --git a/app/service/test/testScript.txt b/app/service/test/testScript.txt new file mode 100644 index 0000000..aefdc86 --- /dev/null +++ b/app/service/test/testScript.txt @@ -0,0 +1 @@ +**Core Elements**\n\n1. **Protagonist:**\n * **Core Identity:** AKIO, a young, spirited male Shiba Inu. His fur is a perfect toasted sesame color. He is naive, driven by simple impulses, and possesses an almost comical level of competitive spirit. He lives a comfortable life near a bustling seaside pier in the present day.\n * **Initial State & Flaw:** Akio begins the story content and carefree, enjoying the sun. His critical flaw is a combination of gluttony and a naive obsession with winning. He believes victory, in any form, is the ultimate measure of happiness and worth, failing to see the emptiness of a prize won without purpose.\n\n2. **The Inciting Incident:**\n * A large, forgotten plate of golden french fries is left on a low crate on the pier. His pier rival, HANA, a sleek red Shiba, spots it at the same moment. An unspoken challenge flashes between them. QUEENIE, an elegant Siamese cat who rules the pier, leaps atop a tall piling, implicitly agreeing to officiate. The contest is suddenly, irrevocably on. This event shatters Akio's peaceful morning and directly triggers his competitive flaw.\n\n3. **The Problem & New Goal:**\n * The problem is brutally simple: How can I eat more fries than Hana and win this contest? This gives Akio a new, all-consuming goal: to achieve victory and be crowned the undisputed champion of the fries.\n\n4. **Conflict & Obstacles:**\n * **Primary Conflict:** Akio vs. Hana in a direct, head-to-head eating competition. Internally, Akio battles his growing physical misery against his desperate, ingrained need to win.\n * **Key Obstacles:**\n 1. Hana is an equally matched and fiercely determined opponent.\n 2. The sheer volume of fries turns the contest from a sprint into a grueling marathon, pushing them both to their physical limits.\n\n5. **The Stakes:**\n * **What is at stake?:** If Akio wins, he gains temporary glory and the satisfaction of his ego. If he fails, he loses face to his rival. However, the true stake is what he stands to permanently lose if he *succeeds* through pure gluttony: he will lose the simple joy of food, his physical well-being, and the potential for a genuine connection with Hana, leaving him bloated and alone with a hollow victory.\n\n6. **Character Arc Accomplished:**\n * By the end, faced with the final fry, Akio overcomes his flaw. He sees the contest's absurdity and Hana's shared suffering. He chooses connection over competition, transforming from a mindless glutton into a creature capable of empathy and grace. He learns that sharing a moment is more fulfilling than winning a prize.\n\n**GENRE:** Sports\n\n---\n\n**SCENE 1**\n\n**[SCENE'S CORE CONFLICT]: The thrill of the challenge and the start of an epic, absurd competition.**\n\nEXT. PIER - DAY\n\n**Scene Transition:** The scene opens on a perfect, sun-drenched morning, establishing the peaceful status quo that is about to be shattered.\n\nThe air is salty and bright. Wooden planks, bleached by the sun, stretch out over gently lapping water. AKIO, a handsome Shiba Inu, basks in a patch of sun, tail curled, eyes blissfully shut.\n\nA WIDE SHOT establishes the key players in their starting positions. A few yards away, HANA, a fiery red Shiba, watches a seagull. On a high wooden piling, QUEENIE, a regal Siamese cat, grooms a paw with aristocratic disdain. On a nearby railing, two PIGEONS, GUS and GERTIE, coo softly.\n\nA careless tourist leaves a large paper plate piled high with golden, glistening FRENCH FRIES on a low wooden crate.\n\nCLOSE-UP on Akio's nose twitching. His eyes snap open. At the same instant, Hana turns. Their eyes lock. The air crackles.\n\nQueenie stops grooming. She stares down at them, then at the fries. With a deliberate, fluid motion, she sits perfectly upright, tail wrapped around her paws. The judge has taken her seat.\n\nAkio's tail begins to thump against the wood. A low, excited growl rumbles in his chest. Hana answers with a sharp, challenging yip.\n\n**AKIO (V.O.)**\nThis was it. The big one.\nGlory was on that plate.\n\nWithout another sound, they both lunge.\n\nRAPID CUTS between Akio and Hana gobbling fries. Their muzzles are a blur of motion. Golden sticks disappear. Tails wag like furious metronomes. The pigeons lean forward, heads cocked, their cooing becoming more rapid and excited.\n\n**GUS & GERTIE**\n(Excited cooing)\n(He's fast! But she's relentless!)\n\nQueenie watches, impassive. She lifts a single paw, as if about to signal a foul, then slowly lowers it. She will allow it. For now.\n\n**AKIO (V.O.)**\nShe was good. Real good.\nBut I was born for this.\n\n---\n\n**SCENE 2**\n\n**[SCENE'S CORE CONFLICT]: The escalation of the contest into an absurd, grueling marathon.**\n\nEXT. PIER - MIDDAY\n\n**Scene Transition:** A time-lapse montage shows the sun climbing higher in the sky. The shadows on the pier shrink. The pile of fries on the plate is noticeably smaller, but a new, larger pile of discarded, half-eaten fries is growing on the planks around it. The transition emphasizes the passage of time and the sheer endurance of the contest.\n\nThe midday sun beats down. The initial frenzy has subsided into a grim, rhythmic battle of attrition. Akio and Hana are no longer gobbling; they are CHEWING. Methodically. Grimly. Their faces are greasy. Their bellies are visibly distended.\n\n**AKIO (V.O.)**\nThe thrill was gone.\nNow, it was just a job.\nOne fry. Then the next.\n\nHana pauses, breathing heavily. A single fry hangs from her mouth. She looks at Akio. There is no fire in her eyes now, only exhaustion. Akio meets her gaze, his own jaw working slowly.\n\nQueenie yawns, a flash of pink mouth and sharp teeth. She looks utterly bored. She stretches one leg, then the other, before settling back into her judicial pose.\n\n**QUEENIE**\n(A low, bored meow)\n(Are they still going? Honestly.)\n\nGus and Gertie are still rapt. They hop from one foot to the other, nudging each other, offering quiet commentary.\n\n**GUS & GERTIE**\n(Murmuring coos)\n(His form is slipping. Look.)\n(She has more stamina. Classic.)\n\nAkio swallows with a gulp. He eyes the remaining pile. It still looks like a mountain. He glances at Hana. She takes a deep breath and doggedly chomps another fry.\n\n**AKIO (V.O.)**\nI couldn't quit. Not now.\nChampions don't quit. Right?\n\nHe forces another fry into his mouth. It tastes like cardboard and regret.\n\n---\n\n**SCENE 3**\n\n**[SCENE'S CORE CONFLICT]: The physical and emotional breaking point, where the cost of victory becomes clear.**\n\nEXT. PIER - SUNSET\n\n**Scene Transition:** The scene opens on an empty shot of the sun, a brilliant orange orb, touching the horizon. The water is a sheet of molten gold. The light is warm but fading, casting long, dramatic shadows. This transition marks the end of the day and the climax of the struggle.\n\nThe pier is bathed in the golden hour's glow. The scene is quiet, the earlier energy completely gone. Akio and Hana are lying on the planks, flanking the plate. They are panting, their sides heaving. The mountain of fries is gone.\n\nOnly ONE. SINGLE. FRY remains.\n\nIt sits perfectly in the center of the greasy plate, a final, golden trophy.\n\nAkio lifts his head. It feels like it weighs a thousand pounds. He looks at the fry, then at Hana. She is a mirror of his own misery. Her fur is matted with grease, her eyes are glassy. A tiny, pathetic whimper escapes her.\n\n**AKIO (V.O.)**\nWe had done it. We ate it all.\nBut there was no cheering.\nJust... this. This quiet ache.\n\nQueenie looks down, a flicker of something—pity? annoyance?—in her blue eyes. She lets out a soft, exasperated sigh.\n\n**QUEENIE**\n(A soft, tired meow)\n(Oh, for heaven's sake. Finish it.)\n\nAkio summons the last of his strength. He begins to drag himself towards the plate. Every muscle screams. This is the final push. The winning point. He can taste victory. It tastes like salt and exhaustion.\n\nHe reaches the plate, his nose inches from the final fry.\n\n---\n\n**SCENE 4**\n\n**[SCENE'S CORE CONFLICT]: The resolution through an act of grace, redefining victory as connection.**\n\nEXT. PIER - DUSK\n\n**Scene Transition:** Continuing directly from the previous scene's climax. The final rays of sunlight disappear, and the cool, soft light of dusk settles over the pier. The shift in light mirrors Akio's internal shift from the fiery heat of competition to the cool clarity of realization.\n\nAkio stares at the last fry. The golden light has faded, and the fry looks pale and unappetizing under the blue twilight.\n\nCLOSE-UP on Akio's eyes. We see the reflection of the lonely fry. Then his eyes shift, looking past it, towards Hana. She hasn't moved. She just lies there, defeated and miserable.\n\n**AKIO (V.O.)**\nAnd then I saw it.\nWinning meant she lost.\nWhat kind of prize was that?\n\nA profound change comes over Akio's face. The grim determination melts away, replaced by a soft, clear understanding.\n\nSLOW MOTION as Akio gently nudges the final fry with his nose. He doesn't eat it. He pushes it, slowly, deliberately, across the greasy plate until it stops directly in front of Hana's nose.\n\nHana's eyes flutter open. She looks at the fry, then at Akio. Confusion, then dawning surprise.\n\nAkio gives a small, tired tail wag. A real one. Not the frantic wag of competition, but a gentle wave of peace.\n\nHana looks at the fry for a long moment. Then she ignores it completely. She inches forward and gently licks Akio's nose.\n\nOn the piling, Queenie watches this. For the first time, a genuine, soft smile seems to grace her feline features. She lets out a quiet, approving purr.\n\n**QUEENIE**\n(A soft, rumbling purr)\n(Finally.)\n\nGus and Gertie coo softly, a gentle, contented sound. They bob their heads in unison, as if applauding.\n\nFADE OUT on the two Shibas, lying side-by-side in the twilight, the single, uneaten fry sitting between them like a forgotten trophy.\n\n**AKIO (V.O.)**\nWe didn't have a winner.\nWe had something better.\n\n---\n\n### Part 2: Addendum for Director & Performance\n\n**【Decoding the Directing Style】**\n\n* **Core Visual Tone:** A mock-epic \"sports documentary\" style. Use grand, sweeping shots in the beginning (like an NFL Films production) contrasted with gritty, handheld close-ups during the \"mid-game\" struggle. The color palette should shift dramatically: vibrant, saturated colors in Scene 1; a harsh, overexposed look in Scene 2; a warm, elegiac \"magic hour\" glow in Scene 3; and a cool, peaceful blue/purple palette for the resolution in Scene 4.\n* **Key Scene Treatment Suggestion:** For the climax in Scene 3, when only one fry remains, the sound design should drop out almost completely. All we hear is the labored breathing of the dogs, the gentle lapping of water, and the distant cry of a gull. The camera should be at a very low angle, making the single fry on the plate look like a monumental obelisk. The final action in Scene 4—Akio pushing the fry—should be captured in a single, unbroken take, focusing on the slow, deliberate movement and Hana's dawning reaction.\n\n**【The Core Performance Key】**\n\n* **The Character's Physicality:** Akio's physicality must arc. He starts with a bouncy, \"on the balls of his feet\" energy. In Scene 2, his movements become heavy and sluggish, his chewing laborious. By Scene 3, he should be practically immobile, every movement an immense effort. His final act of pushing the fry should be gentle and tender, a stark contrast to the frantic gobbling at the start.\n* **Subtextual Drive:** Akio's internal voiceover is the text; the subtext is in his eyes and body. His V.O. in Scene 2 says \"Champions don't quit,\" but his eyes should scream, \"I want my mommy.\" The subtext of the final scene is a silent apology and an offering of peace. When Hana licks his nose, the subtext is \"I accept. We're okay.\"\n\n**【Connection to the Zeitgeist】**\n\n* This story of a pointless, all-consuming competition for a meaningless prize serves as a gentle parable for the modern obsession with \"winning\" on social media and in hustle culture, suggesting that true fulfillment is found not in victory, but in shared humanity and connection. diff --git a/app/service/usecase/ScriptEditUseCase.ts b/app/service/usecase/ScriptEditUseCase.ts index 29d4f44..83764f9 100644 --- a/app/service/usecase/ScriptEditUseCase.ts +++ b/app/service/usecase/ScriptEditUseCase.ts @@ -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 返回项目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 + */ + async saveScript(projectId: string): Promise { + 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 */ - async applyScript(projectId: string): Promise { + async applyScript(projectId: string,planId: string): Promise { 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 diff --git a/lib/auth.ts b/lib/auth.ts index eafd945..1e111fa 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -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; -}; \ No newline at end of file +};