// Mock localStorage global.localStorage = { getItem: jest.fn(() => "mock-token"), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), length: 0, key: jest.fn(), }; // Mock BASE_URL jest.mock("../../../api/constants", () => ({ BASE_URL: "https://77.smartvideo.py.qikongjian.com", })); import { getProjectScript, } from "../../../api/video_flow"; import {StoryDetails } from "../domain/valueObject"; import { ScriptEditUseCase } from "../usecase/ScriptEditUseCase"; describe("ScriptService 业务逻辑测试", () => { // 创建新的剧本编辑用例 const newScriptEditUseCase = new ScriptEditUseCase(""); const script = "在阳光明媚的码头上,两只柴犬展开了一场薯条吃比赛。一只优雅的母猫担任裁判,端坐高处,威严地监督比赛。两只鸽子站在一旁,歪着头有趣地看着,偶尔咕咕低鸣。柴犬们瞪大眼睛,尾巴摇得飞快,争抢盘子里的金黄薯条。从日出到天黑,它们吃个不停,薯条堆成了小山,母猫无奈摇头,鸽子仍兴致勃勃,场面热闹非凡。"; let projectId: string; // let name: string; it("想法生成剧本", async () => { const res = await newScriptEditUseCase.generateScript(script, (content) => { // console.log(content); console.log(newScriptEditUseCase.getStoryDetails()); }); expect(newScriptEditUseCase.toString()).toBeDefined(); }, 300000); it("创建项目", async () => { const createRes = await newScriptEditUseCase.createProject( script, "user123", "automatic", "720p", "en", "10" ); expect(createRes.project_id).toBeDefined(); projectId = createRes.project_id; }); it("保存剧本", async () => { const res = await newScriptEditUseCase.saveScript(projectId); console.log(res); }); it("应用剧本", async () => { await newScriptEditUseCase.applyScript(projectId); console.log(projectId); }); }); describe("解析测试", () => { const scriptText = `"** Core Elements** \n\n1. Protagonist:\n\nCore Identity: LEO (24), male, a quiet and introverted app developer. He has a kind face but carries a perpetual tension in his shoulders. He lives in a modern, impersonal city. His personality is anxious, thoughtful, and overly self-critical.\n\nInitial State & Flaw: Leo is sitting alone, waiting for a first date he's genuinely excited about. His core flaw is a crippling fear of inadequacy and social rejection. He believes he is fundamentally uninteresting and that people will inevitably find him wanting, causing him to hide behind a curated digital persona.\n\n2. The Inciting Incident:\n\n(State Clearly) His date, CHLOE (23), arrives, but she is not alone. She is accompanied by MARK (25), a confident, handsome man whom she introduces as just a \"friend.\" This event shatters Leo's hopeful anticipation and directly preys on his fear that he is not enough on his own.\n\n3. The Problem & New Goal:\n\n(State Clearly) The problem is no longer about winning a date; it's about surviving a socially excruciating and humiliating situation. Leo's new, urgent goal is to navigate this tense encounter, understand what is truly happening, and find a way to exit with his dignity intact.\n\n4. Conflict & Obstacles:\n\nPrimary Conflict: The conflict is both interpersonal and internal. Leo vs. Mark (a battle for presence and respect). Leo vs. Chloe (her perceived cruelty or obliviousness). And most importantly, Leo vs. Himself (his deep-seated social anxiety versus a nascent desire for self-respect).\n\nKey Obstacles: 1) The public setting of the coffee shop prevents any honest emotional outburst. 2) Mark's charismatic dominance of the conversation constantly sidelines Leo.\n\n5. The Stakes:\n\nWhat is at stake?: If Leo succeeds in asserting himself, he gains a monumental victory over his internal demons, proving to himself that his worth isn't determined by others' approval. If he fails—by crumbling or fleeing without a word—he will permanently reinforce his self-loathing belief that he is inadequate, losing not just a potential relationship, but his own chance at self-respect.\n\n6. Character Arc Accomplished:\n\n(State the Result Clearly) Leo achieves his arc by making a choice rooted in self-worth, not fear. Instead of enduring the humiliation or running away, he calmly and directly addresses the absurdity of the situation, pays for his drink, and leaves. This quiet act of defiance is his triumph. He has transformed from someone fearing rejection to someone who rejects an unacceptable situation.\n\n**GENRE:** Drama\n\n---\n\n**SCENE 1**\n\nINT. \"THE DAILY GRIND\" COFFEE SHOP - DAY\n\n**Scene Transition:** An establishing shot of the coffee shop, a trendy place with exposed brick and the low HUM of conversation. The camera finds our character already in place, his presence setting the scene's initial, anxious tone.\n\nSunlight, thick with dust motes, streams through a large window. The air smells of burnt coffee and steamed milk.\n\nLEO (24), neat but nervous, sits at a small table for two. His posture is closed-off, shoulders hunched.\n\nAn untouched cappuccino sweats before him.\n\nHis phone is in his hands. An oasis.\n\nEXTREME CLOSE-UP - PHONE SCREEN\nA messaging app is open. A conversation with \"Chloe.\" Her profile picture is bright, confident. His last message, \"Here! Got a table by the window :)\" was sent 15 minutes ago. It is marked \"Read.\"\n\nLeo's thumb hovers, composing a new message, then deleting it. He locks the screen. Unlocks it. A nervous tic.\n\nHe looks toward the door. Each time it opens with a JINGLE of a small bell, his shoulders tense. A family enters. He slumps back down.\n\nCLOSE-UP - LEO\nHis eyes are full of a painful, familiar hope.\n\n**LEO (V.O.)**\nJust be normal.\nDon't say anything weird.\nPlease just like me.\n\nHe forces a sip of his now-lukewarm cappuccino. He grimaces slightly. The bell JINGLES again. He doesn't look up this time, protecting himself from another false alarm.\n\n**CHLOE (O.S.)**\nLeo?\n\nLeo's head snaps up.\n\nCHLOE (23) stands there. She matches her photo. Bright, energetic. A wave of relief washes over Leo's face. He starts to stand, a smile forming.\n\nThen he sees him.\n\nMARK (25), handsome and effortlessly cool, stands beside her. He scans the cafe with an owner's ease. Chloe's hand is briefly on his arm.\n\nLeo freezes mid-motion, half-standing. The smile dies on his lips.\n\n---\n\n**SCENE 2**\n\nINT. \"THE DAILY GRIND\" COFFEE SHOP - MOMENTS LATER\n\n**Scene Transition:** The characters are now locked in their initial, awkward arrangement. The previous scene's hope has curdled into the raw tension that will drive this new scene.\n\nLeo slowly sinks back into his chair. Chloe and Mark approach the small table. The space immediately feels crowded, invasive.\n\n**CHLOE**\n(A bit too cheerful)\nSorry we're late.\nRan into my friend, Mark.\nThis is Mark.\n\nMark offers a lazy, confident smile. He doesn't offer to shake hands.\n\n**MARK**\nHey, man. What's up?\n\n**LEO**\n(Muted)\nHey. Leo.\n\nMark pulls a chair from a nearby table. He sits beside Chloe, not across from her. They form a unit. Leo is the satellite.\n\n**CHLOE**\n(To Mark)\nI told you he was cute.\n(To Leo)\nI hope you don't mind.\nHe was just heading this way.\n\nThe question isn't a question. It's a statement. Leo just nods, a knot forming in his stomach. The barista calls an order. The sound is jarringly loud.\n\n**MARK**\n(Leaning back)\nSo. Chloe tells me you make apps.\nLike, games and stuff?\n\n**LEO**\nNo. Mostly productivity tools.\nOrganizational software.\n\nMark smirks, sharing a private look with Chloe.\n\n**MARK**\nRight. Fun stuff.\n\nThe subtext is a hammer blow. Leo's hands retreat from the table, hiding in his lap. He feels his face flush with heat.\n\n---\n\n**SCENE 3**\n\nINT. \"THE DAILY GRIND\" COFFEE SHOP - LATER\n\n**Scene Transition:** Five minutes have passed. The forced conversation has created a palpable sense of dread. The camera pushes in slightly, tightening the frame to emphasize Leo's growing claustrophobia.\n\nEmpty sugar packets are shredded into a small pile by Leo's cup. A nervous habit made manifest.\n\nMark is telling a story. It's loud, animated. He and Chloe are laughing. Leo is a spectator at his own date.\n\n**MARK**\n...so I tell the guy, that's not a bug.\nThat's a feature!\n\nChloe laughs, a genuine, loud laugh. It's a sound Leo thought was reserved for him.\n\n**CHLOE**\n(Wiping a tear)\nYou're terrible.\nHe actually believed you?\n\n**MARK**\nFor a second.\nHe was a coder type. Like Leo.\nYou know. A bit literal.\n\nMark winks at Leo. It's not friendly. It's a display of dominance.\n\nThis is the turning point.\n\nCLOSE-UP - LEO\nThe anxiousness in his eyes doesn't vanish. It crystallizes. It hardens into something new. A quiet, cold clarity. The fog of his fear has burned away, revealing the stark, humiliating landscape.\n\nHe stops shredding the packet. His hands become perfectly still on the table.\n\n**LEO (V.O.)**\nOh.\nI see now.\n\nHe looks from Mark's smug face to Chloe's smiling one. She doesn't meet his gaze. She is completely absorbed by Mark.\n\nLeo slowly, deliberately, reaches for his wallet.\n\n---\n\n**SCENE 4**\n\nINT. \"THE DAILY GRIND\" COFFEE SHOP - MOMENTS LATER\n\n**Scene Transition:** Building directly from Leo's internal decision, this scene is the physical execution of that choice. The stillness of the previous shot is broken by his decisive action.\n\nThe sound of a chair scraping against the floor cuts through their laughter.\n\nLeo is standing. His posture is different. Shoulders back. Chin up. He is no longer hunched.\n\nChloe and Mark look up, surprised by the interruption.\n\n**CHLOE**\nYou okay?\nGoing to the bathroom?\n\nLeo places a ten-dollar bill on the table. Enough for his coffee and a generous tip. The gesture is precise. Final.\n\n**LEO**\nI don't think I am.\n\nHe looks directly at Chloe. His gaze is not angry. It is clear.\n\n**LEO**\nThis was a mistake.\nEnjoy your coffee.\n\nHe doesn't look at Mark. He doesn't need to. Mark is irrelevant to this decision.\n\nLeo turns and walks toward the door. His steps are even, measured. Not a retreat. An exit.\n\nThe bell on the door JINGLES softly as he leaves.\n\nThe camera holds on the table. On the shocked, confused faces of Chloe and Mark. For the first time, they are silent. The ten-dollar bill sits between them, an indictment.\n\nFADE TO BLACK.\n\n---\n\n### Part 2: Addendum for Director & Performance\n\n**【Decoding the Directing Style】**\n\n* **Core Visual Tone:** A warm, inviting coffee shop palette (ambers, browns) that feels increasingly suffocating. Use a shallow depth of field to keep Leo emotionally isolated, with the background and even other characters often soft-focused, reflecting his internal, anxious world. The camera should be mostly static and observational, making his final, deliberate movements feel seismic.\n\n* **Key Scene Treatment Suggestion:** In Scene 4, when Leo stands, the camera should be at a low angle, making him appear taller and more resolute than before. The action of him placing the money on the table should be captured in an EXTREME CLOSE-UP, the sound of the bill hitting the wood amplified. After he leaves, hold a WIDE SHOT on Chloe and Mark at the table, emphasizing the new, awkward space he has left behind. The blocking is a full reversal: he begins trapped *by* the table and ends by using the table as the stage for his departure.\n\n**【The Core Performance Key】**\n\n* **The Character's Physicality:** Leo's performance is a journey from contraction to expansion. He begins hunched, making himself small, hands hidden or fidgeting (shredding packets). His core action is avoidance. His transformation is marked by stillness (Scene 3) followed by deliberate, open movement (Scene 4). His final walk should be straight-backed, a stark contrast to his initial posture.\n\n* **Subtextual Drive:** For the first three scenes, Leo's subtext is a desperate plea: \"Please see me. Please choose me. Am I good enough?\" In Scene 3, after Mark's \"coder type\" comment, the subtext shifts instantly to a declarative statement: \"I see you. And I am better than this.\" His final lines are not an attack, but the calm, verbal expression of that new internal truth.\n\n**【Connection to the Zeitgeist】**\n\n* This story captures the profound anxiety and disposability inherent in modern app-based dating, where individuals are often treated as interchangeable options rather than human beings deserving of basic respect."`; let protagonistText = "", incitingIncidentText = "", categories: string[] = [], protagonist = "", problem = "", conflict = "", stakes = ""; const s = new StoryDetails(""); it("剧本解析", async () => { protagonistText = s.extractContentByHeader(scriptText, "Logline"); console.log("protagonistText", protagonistText); incitingIncidentText = s.extractContentByHeader( scriptText, "The Inciting Incident" ); console.log("incitingIncidentText", incitingIncidentText); // --- 其他字段的提取保持不变,使用标题进行查找 --- categories = s .extractContentByHeader(scriptText, "GENRE") .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); console.log("categories", categories); protagonist = s.extractContentByHeader(scriptText, "Core Identity:"); console.log("protagonist", protagonist); problem = s.extractContentByHeader(scriptText, "The Problem & New Goal"); console.log("problem", problem); conflict = s.extractContentByHeader(scriptText, "Conflict & Obstacles"); console.log("conflict", conflict); stakes = s.extractContentByHeader(scriptText, "The Stakes"); console.log("stakes", stakes); }); }); describe("剧本功能对接测试",()=>{ it("初始化 解析剧本" , async()=>{ const response = await getProjectScript({ project_id: "21f194df-cb4b-4e3a-8d44-ca14f23fd1c2" }); const newScriptEditUseCase = new ScriptEditUseCase(response.data.generated_script); }) it("分类解析",()=>{ // 测试代码 const testText = `**GENRE:** Drama\n\n---\n\n**SCENE 1**`; const s = new StoryDetails(""); const result = s.extractContentByHeader(testText, "GENRE", true); console.log("测试结果:", result); // 应该输出 "Drama" }) } )