feat(web): refactor home preset cards to 16:9 poster style with titles and tags below cover
@@ -3,6 +3,14 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/* ============================================================================
|
||||
InfiPlot · 首页(编辑式视觉风格 · 居中构图,呼应低保真原型)
|
||||
- 顶部 Header:左上角衬线 wordmark logo
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/* ============================================================================
|
||||
InfiPlot · 首页(编辑式视觉风格 · 居中构图,呼应低保真原型)
|
||||
- 顶部 Header:左上角衬线 wordmark logo
|
||||
@@ -11,6 +19,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
- 项目介绍(题跋式排版)
|
||||
========================================================================== */
|
||||
|
||||
|
||||
type Gender = "男性向" | "女性向";
|
||||
|
||||
const EXAMPLE_PHRASES: Record<Gender, string[]> = {
|
||||
@@ -41,18 +50,30 @@ const OPTS: Opt[] = [
|
||||
modal: true,
|
||||
items: [
|
||||
"自动",
|
||||
"二次元",
|
||||
"吉卜力",
|
||||
"真实系",
|
||||
"超写实",
|
||||
"水彩",
|
||||
"像素风",
|
||||
"日系动画",
|
||||
"3D 渲染",
|
||||
"蒸汽朋克",
|
||||
"玄幻",
|
||||
"国风水墨",
|
||||
"赛博朋克",
|
||||
"古典厚涂油画 (学术奇幻)",
|
||||
"极简中国水墨 (Image 0参考升级版)",
|
||||
"浮世绘",
|
||||
"莫高窟壁画风 (敦煌学)",
|
||||
"镶嵌画 (拜占庭/马赛克)",
|
||||
"彩绘玻璃 (哥特风)",
|
||||
"吉卜力治愈手绘",
|
||||
"京阿尼细腻日常",
|
||||
"新海诚唯美光影 (Image 2参考)",
|
||||
"赛博朋克 / 赛璐珞二次元",
|
||||
"Galgame CG 梦幻光影",
|
||||
"3D 动漫电影质感",
|
||||
"蒸汽波 (Vaporwave) 赛璐珞",
|
||||
"波普艺术 (Pop Art)",
|
||||
"故障艺术 (Glitch Art)",
|
||||
"剪纸艺术 (Papercut)",
|
||||
"科幻:太阳朋克 (Solar Punk)",
|
||||
"奇幻:爱手艺 (Lovecraftian Horror)",
|
||||
"现代惊悚:霓虹剪影 (Urban Noir)",
|
||||
"温馨推理:英式村庄 (Cozy Mystery)",
|
||||
"哥特言情:庄园废墟 (Gothic Romance)",
|
||||
"格林童话:暗黑森林 (Fairytale Noir)",
|
||||
"废土科幻 (Post-Apocalyptic)",
|
||||
"都市幻想:隐形世界 (Urban Fantasy)"
|
||||
],
|
||||
},
|
||||
{ label: "剧情风格", items: ["平铺直叙", "多线转折", "悬疑烧脑", "治愈日常"], defaultIndex: 1 },
|
||||
@@ -60,95 +81,522 @@ const OPTS: Opt[] = [
|
||||
{ label: "内容节奏", items: ["慢热细腻", "紧凑爽快"], defaultIndex: 1 },
|
||||
];
|
||||
|
||||
type StoryContent = { title: string; outline: string; style: string };
|
||||
type StoryContent = { title: string; outline: string; style: string; tags: string[] };
|
||||
|
||||
const STYLE_MAP: Record<string, string> = {
|
||||
二次元: "唯美二次元动漫插画,日系 galgame 精致质感,柔和温暖的自然光照。",
|
||||
吉卜力: "吉卜力工作室风格,手绘动画质感,柔和水彩底色,温暖治愈的氛围。",
|
||||
真实系: "真实电影感,柔和自然光照,胶片颗粒。",
|
||||
超写实: "超写实人像与场景,电影级布光,皮肤与材质细节精致。",
|
||||
水彩: "水彩插画,湿润晕染笔触,纸纹底色。",
|
||||
像素风: "像素风格,复古游戏 16-bit 调色,方块化几何造型。",
|
||||
日系动画: "现代日系动画 cel-shading,硬光阴影分层,赛璐璐风。",
|
||||
"3D 渲染": "3D 渲染卡通风格,柔和次表面散射,干净的电影级布光。",
|
||||
蒸汽朋克: "蒸汽朋克美学,铜色齿轮与蒸汽,工业革命氛围。",
|
||||
玄幻: "国风玄幻插画,仙气缭绕,群山烟雨与神兽萦绕。",
|
||||
国风水墨: "国潮唯美古风插画,水墨微晕渲染,仙侠浪漫色彩,极具东方神韵。",
|
||||
赛博朋克: "赛博朋克都市,霓虹反射湿润街道,电子义体高光。",
|
||||
"古典厚涂油画 (学术奇幻)": "Dark fantasy oil painting style, a sprawling clockwork steampunk city built into a mountain range at twilight, immense gothic spires with glowing green lamps, complex gears and platforms. Richly detailed, impasto texture, dramatic academic lighting. A grand airship arrives at a high dock. Horizontal composition with massive clear dark sky for typography.",
|
||||
"极简中国水墨 (Image 0参考升级版)": "Minimalist Chinese ink wash style, a lone immortal cultivator sitting on a precipice, facing an endless sea of clouds and distant jagged peaks. Ethereal, sparse composition with poetic brushstrokes, monochrome palette with subtle blue hints. Very large blank mist area for text placement.",
|
||||
"浮世绘": "Ukiyo-e woodblock print style, a majestic red and gold phoenix with elaborate trailing feathers rising above a wave-crested dark blue sea, Mount Fuji visible through cherry branches. Bold outlines, flat colors with paper texture, ancient and mystical atmosphere. Central clear area in the sea and sky for typography.",
|
||||
"莫高窟壁画风 (敦煌学)": "Dunhuang fresco style, a celestial apsaras flying with flowing scarves, holding a Lute, surrounded by stylized lotus flowers and floating geometric patterns on an aged stucco wall. Muted, oxidized mineral colors, delicate line art, historical and divine ambiance. Side vertical area cleared for titles.",
|
||||
"镶嵌画 (拜占庭/马赛克)": "Byzantine mosaic style, an iconic portrait of a warrior saint with golden armor and a halo, composed of thousands of small, glittering glass tesseræ. Deep blues and golds, spiritual and ancient feel, flat background. Background field of gold tiles left blank for text.",
|
||||
"彩绘玻璃 (哥特风)": "Stained glass style, a depiction of a griffin battling a serpent, framed by gothic archways and trefoils. Vibrant, translucent jewel colors, bold black leading lines. The image should look like an ancient window panel. Outer panels of plain blue glass left clear for text.",
|
||||
"吉卜力治愈手绘": "Ghibli hand-painted watercolor style, a detailed concept art of a girl and her small companion creature running through a vast wildflower meadow toward a fantastical airship. Natural daylight, soft washes, nostalgic feel. Upper left sky area is negative space for typography.",
|
||||
"京阿尼细腻日常": "KyoAni anime style, fine line art, a detailed high school girl sitting by a library window during light rain, warm library light contrasting the cool moonlight outside. Deep emotional atmosphere, delicate expression. Empty right-side foreground area for title.",
|
||||
"新海诚唯美光影 (Image 2参考)": "Makoto Shinkai anime style, hyper-detailed, a wide panoramic night view of a glowing cherry tree under a dramatic starry sky with a comet trail, a lonely high school girl in a uniform looking up. Brilliant lighting effects, vivid colors. Significant blank space in the upper atmosphere for text.",
|
||||
"赛博朋克 / 赛璐珞二次元": "Cyberpunk anime style, cel-shaded animation, a tech-wear protagonist standing on a rainy rooftop, looking out at a dense, neon-drenched futuristic megacity with flying vehicles. Hard edges, high saturation, sharp contrast. Massive upper background sky area for title placement.",
|
||||
"Galgame CG 梦幻光影": "High-quality Galgame CG illustration, a dreamlike beach scene with sparkling waves, a beautiful girl with pastel pink hair in a white summer dress smiling warmly. Pastel colors, bloom lighting, clean composition, soft focus. Significant negative space in the sky and sea area for text.",
|
||||
"3D 动漫电影质感": "Cinematic 3D animated film style (like Makoto Shinkai or Pixar), a high-resolution render of a young boy pilot fixing a small propeller plane in a rustic hangar at sunrise. Volumetric lighting, warm colors, deep textures, cinematic composition. Blank wall space and open doorway area for text.",
|
||||
"蒸汽波 (Vaporwave) 赛璐珞": "Vaporwave aesthetic, anime style, a nostalgic portrait of a character with purple hair wearing sunglasses, a geometric grid floor and palm trees, background sunset over a purple ocean. Glitch effects, soft neon pink and blue palette, retro feel. Blank foreground grid area for title.",
|
||||
"波普艺术 (Pop Art)": "Pop Art style illustration, a close-up of a glamorous woman with red lips and a speech bubble with an exclamation point, rendered with comic book dots and bold outlines. High-saturation contrasting colors. Speech bubble and large background color blocks left blank for text.",
|
||||
"故障艺术 (Glitch Art)": "Glitch art style portrait, a character profile distorted by data corruption, pixel sorting, and digital artifacts in cyan, magenta, and yellow. Cybernetic, high-tech and moody atmosphere. Dark, uncorrupted negative space in the upper background for typography.",
|
||||
"剪纸艺术 (Papercut)": "Multilayered papercut art style, a 3D landscape of a deep forest and a fairytale castle, made of staggered paper layers with intricate cutouts. Backlighting, soft shadows, dimensional depth. Blank background layer cleared for title placement.",
|
||||
"科幻:太阳朋克 (Solar Punk)": "Solar Punk art style, a wide view of a sustainable, futuristic city integrated with dense green rooftop gardens and vertical farms, illuminated by clean solar and wind energy. Bright, optimistic lighting, organic textures. Large foreground plaza area cleared for titles.",
|
||||
"奇幻:爱手艺 (Lovecraftian Horror)": "Dark cosmic horror illustration, a lone explorer stands on a desolate shore, gazing at a massive, ancient, indescribable eldritch entity rising from a stormy sea. Moody, muted cool colors, dramatic lighting, visible brushstrokes. The dark, stormy sky quadrant left completely blank for text.",
|
||||
"现代惊悚:霓虹剪影 (Urban Noir)": "Modern urban noir, a minimalist silhouette of a man in a trench coat, standing in a dark, wet alleyway under a single buzzing neon sign reflecting on puddles. High contrast, cinematic noir lighting, deep shadows. The wet cobblestone ground left mostly dark for typography.",
|
||||
"温馨推理:英式村庄 (Cozy Mystery)": "Cozy mystery book cover illustration, a charming, warm English village scene at night, snow on the thatched roofs, golden light from a bookstore window, and a single cat perched on a fence. Comforting and mysterious feel. Significant background sky and foreground pavement area for title.",
|
||||
"哥特言情:庄园废墟 (Gothic Romance)": "Gothic romance illustration, a wide panoramic view of a young woman in a flowing dark velvet dress, standing before the desolate, moonlit ruins of a grand gothic manor on a foggy cliff. Muted greys and blues, romantic and melancholic. The upper background cliff and sky for bold titles.",
|
||||
"格林童话:暗黑森林 (Fairytale Noir)": "Dark fairytale illustration, a wide shot of a small girl in a red cloak walking into a massive, dark, twisted ancient forest where the trees look like claws. Grimm's style, classical illustration, mood of awe and dread. The dark foreground forest ground left blank for text.",
|
||||
"废土科幻 (Post-Apocalyptic)": "Post-apocalyptic landscape illustration, a vast desert wasteland with the rusted remains of overgrown highway and a fallen Statue of Liberty in the distance under a dusty orange sky. Muted cool and warm colors. Significant clear ground and sky area for text.",
|
||||
"都市幻想:隐形世界 (Urban Fantasy)": "Urban fantasy concept art, a detailed view of a hidden, glowing magical pathway revealed underneath a busy modern pedestrian bridge in a rain-streaked metropolitan city. Contrast of mundane and magical. Minimal detail in the wet street foreground and upper sky for titles."
|
||||
};
|
||||
|
||||
/* 每个性向 32 篇预设剧情(红果短视频式开场钩子)。与封面 /home/{m|f}{i}.webp 按索引
|
||||
一一对应;style 字段决定点卡片进入 /play 时使用的画风(对应 styleMap 的 12 种风格)。
|
||||
/* 每个性向 24 篇预设剧情(与封面 /home/{m|f}{i}.webp 按索引一一对应)。
|
||||
男/女同索引共享画面尺寸,切性向 crossfade 时卡片高度不跳变。 */
|
||||
const STORIES: Record<Gender, StoryContent[]> = {
|
||||
男性向: [
|
||||
{ title: "战神归来", outline: "五年前我战死边境,灵柩送回家时她抱着儿子改嫁了。今天我站在他们的婚礼门口,新郎刚要骂人,跪在他面前的二十个保镖喊了我一声「上将」。", style: "真实系" },
|
||||
{ title: "神医归乡", outline: "在城里被嘲笑成「江湖野医生」的我,回了一趟老家。村口的老人见到我直接哭了:「您终于回来了,您当年的师父…病了。」其实他们不知道,我现在是国手第一。", style: "吉卜力" },
|
||||
{ title: "赘婿亮剑", outline: "岳父大寿,我端着茶被全场嫌弃,一句「废物」让我滚出去。门外停着九辆悬挂军牌的劳斯莱斯,下来的人朝我深深一鞠躬:「少爷,集团等您回去签字。」", style: "真实系" },
|
||||
{ title: "送外卖的少主", outline: "你以为我是给你送了三个月外卖的那个小哥?昨晚有人对我说:「少主,您隐姓埋名的三年,到了。」——而你昨天还笑我连一杯咖啡都买不起。", style: "二次元" },
|
||||
{ title: "兵王食言", outline: "退役那天我答应过队长:「这辈子不再开枪。」但你今天在我面前打了她一巴掌,那我食言一次。", style: "真实系" },
|
||||
{ title: "重生分手前夜", outline: "凌晨四点,我醒在我们分手的那个夜晚——她正打开门要走。这一次,我先把戒指递了出去:「分手,但戒指你拿好,下个月你会用到它。」", style: "日系动画" },
|
||||
{ title: "重生回到高考前", outline: "我重生回到高考前一周。这一次,我提前知道了每一道压轴题,也知道了——三天后,她会在天台上跳下去。", style: "吉卜力" },
|
||||
{ title: "墓前签到", outline: "我每天去亡妻的墓地签到,第七天,系统弹出一行字:「奖励到账:未亡人 × 1。」墓碑后走出一个长得和她一模一样的姑娘:「你是…谁?」", style: "二次元" },
|
||||
{ title: "凌晨四点抽卡", outline: "凌晨三点,我十连抽 SSR 出货,光柱从屏幕里溢出来。客厅响起脚步声,一个穿着我 T 恤的女人揉着眼睛走出来:「老公,你也太晚了。」", style: "3D 渲染" },
|
||||
{ title: "系统选妃", outline: "系统给了我七个未婚妻候选,每错一个,地图上就有一座城被抹掉。倒计时 30 秒,她们七个同时朝我看过来。", style: "二次元" },
|
||||
{ title: "穿成废柴皇子", outline: "睁眼是冷宫废柴皇子,太监正在念赐死圣旨。我笑了——上辈子读的那本《这就是大唐》,是我自己写的。", style: "国风水墨" },
|
||||
{ title: "穿成乙游男配", outline: "我穿成了乙游里第一章就被处刑的反派男配。倒计时三个月。可女主她…昨天竟然主动来找我了。", style: "二次元" },
|
||||
{ title: "毒酒之后", outline: "睁眼是 1928 年,我刚被亲弟弟下毒,倒在少帅府的红毯上。门外军靴声逼近——他来确认我是不是真死了。", style: "真实系" },
|
||||
{ title: "九重雷劫", outline: "修了三百年,今夜九重雷劫降下。第八道劫雷劈开时,我看见劫云之上,那个一直在偷偷护我的人,竟是她。", style: "玄幻" },
|
||||
{ title: "山门扫地僧", outline: "我在山门扫地三十年,谁都看不起我。今日魔尊踏破山门,宗主跪地求饶。我抬头:「让一让,我去扫他。」", style: "国风水墨" },
|
||||
{ title: "末世第一夜", outline: "同寝的兄弟开始啃我的脖子。我抬手将他甩开——指尖滴下的血珠悬在半空,凝结成了一柄银白小剑。", style: "真实系" },
|
||||
{ title: "雷霆觉醒", outline: "雷劈不死的第七天,我握紧了拳头。掌心炸开一道闪电,把面前的丧尸群一齐劈成了灰。", style: "赛博朋克" },
|
||||
{ title: "家宴镇压", outline: "家宴上岳父冷笑:「你也敢上桌?」我手机震了一下,是父亲发来的:「儿,神州七大家主,已到楼下。」", style: "真实系" },
|
||||
{ title: "买葱归来", outline: "二十年前那场天工大会上消失的人——今天回菜市场买葱,被小贩多收了两毛。他笑了:「这二十年的利息,连本带利,今晚一起还。」", style: "国风水墨" },
|
||||
{ title: "红盖头之下", outline: "敌对家族送来一个新娘,遮着红盖头。我掀开那一刻,下面是和我死去的妹妹一模一样的脸。她抬眼:「哥…你别杀我。」", style: "超写实" },
|
||||
{ title: "上海双面谍", outline: "1936 年。军统让我潜入日方,日方让我潜入军统。今晚——他们要见面,而我必须同时出现在两间房里。", style: "真实系" },
|
||||
{ title: "比武场的茶博士", outline: "比武大会上,我端着茶水路过,宗主们的剑突然全都举不起来了。我抬眼:「老衲只是看不下去你们吵架。」", style: "国风水墨" },
|
||||
{ title: "高考前夜", outline: "全市模考垫底的我,高考前夜被四个西装男按在桌前:「这次,你必须考第一。」原来——我爸是教育部的人。", style: "日系动画" },
|
||||
{ title: "失踪一年", outline: "我被宣告死亡 12 个月后,背着血迹斑斑的包,站在了她婚礼现场的门口。新郎认出我,杯子摔到了地上。", style: "真实系" },
|
||||
{ title: "天台堵她", outline: "学校最不好惹的那位转学生,第一天就堵了我的天台。我把她书包一扯——里面掉出来一沓我从小写的情书。", style: "日系动画" },
|
||||
{ title: "转学第一天", outline: "转学第一天,年级第一坐我后桌。下课她把试卷拍在我面前:「这道题,你为什么写得和我答案一字不差?」", style: "二次元" },
|
||||
{ title: "无职觉醒", outline: "成年礼上全班觉醒职业,只有我天命「无职」。所有人嘲笑我的时候,光柱从我身上炸开——觉醒结果:「神」。", style: "玄幻" },
|
||||
{ title: "草稿纸里的我", outline: "睁眼发现自己是一张草稿纸上的火柴小人,住在 16-bit 的网格世界里。橡皮擦从天而降,正在抹掉这一行字——也包括我。", style: "像素风" },
|
||||
{ title: "云上的国家", outline: "齿轮轰鸣的飞艇甲板上,独眼船长把望远镜递到我手里:「云的那一头有个国家,专门关像你这样的人。」", style: "蒸汽朋克" },
|
||||
{ title: "舰桥上的少年", outline: "殖民母舰只剩 30 秒,主炮指挥官的椅子是空的。舰长抬眼看着 17 岁的我:「上去。整个人类,就交给你了。」", style: "赛博朋克" },
|
||||
{ title: "末节队长服", outline: "全联盟都骂我废柴,机甲赛决赛末节,教练把队长徽章按在我手里:「上去,把这局赢回来——这一台,是人类最后的机甲。」", style: "赛博朋克" },
|
||||
{ title: "学长的真面目", outline: "三年青梅当众接过富二代的玫瑰,转身扑进他怀里。我笑了笑——明天,是我接手父亲那个上市公司的日子。", style: "真实系" },
|
||||
],
|
||||
{
|
||||
"title": "贤者陨落",
|
||||
"outline": "我曾是支撑帝国的九环大贤者,却因研究禁忌魔法被诬陷为叛徒,在万众唾弃中被放逐。十年后,深渊裂隙撕裂天空,昔日陷害我的圣子却跪在我面前,求我拯救这个已忘记我的世界。",
|
||||
"style": "古典厚涂油画 (学术奇幻)",
|
||||
"tags": [
|
||||
"修仙",
|
||||
"逆袭",
|
||||
"打脸"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "水墨斩龙人",
|
||||
"outline": "我本是个在终南山画山水的落魄画师,直到一笔画出的墨龙活了过来。当朝廷派人抓我时,我随手泼墨,十万禁军被一卷《江山万里图》吸入画中。从此,人间多了一位以笔为剑的斩龙人。",
|
||||
"style": "极简中国水墨 (Image 0参考升级版)",
|
||||
"tags": [
|
||||
"异能",
|
||||
"装逼",
|
||||
"都市玄幻"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "花魁的刀",
|
||||
"outline": "我是江户最负盛名的花魁,琴棋书画样样精通。但没人知道,每晚来我房中「听曲」的幕府将军,其实是在向我汇报:那些试图颠覆幕府的浪人,昨夜又被我的忍者暗杀了多少。",
|
||||
"style": "浮世绘",
|
||||
"tags": [
|
||||
"扮猪吃虎",
|
||||
"虐渣",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "飞天乐神",
|
||||
"outline": "我在敦煌壁画中沉睡了千年,直到考古队的探照灯惊醒了我。走出壁画的那一刻,现代都市的霓虹让我眩晕,而追捕我的特勤队长,在看到我手中琵琶时,却颤抖着喊出了我千年前的封号。",
|
||||
"style": "莫高窟壁画风 (敦煌学)",
|
||||
"tags": [
|
||||
"穿越",
|
||||
"都市玄幻",
|
||||
"甜宠"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "圣像的谎言",
|
||||
"outline": "作为拜占庭帝国的黄金镶嵌师,我亲手为皇帝打造了镶嵌满宝石的圣像。但当我在像眼深处刻下足以毁灭帝国的诅咒密文时,皇帝还以为这不过是又一件彰显神威的艺术品。",
|
||||
"style": "镶嵌画 (拜占庭/马赛克)",
|
||||
"tags": [
|
||||
"虐渣",
|
||||
"暗黑童话",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "血色玫瑰",
|
||||
"outline": "我是哥特大教堂最年轻的彩窗工匠,也是血族最后的子嗣。每当阳光穿透我绘制的玫瑰花窗,圣光便会灼伤我的皮肤。但主教不知道,我绘入彩窗的不是圣经故事,而是如何打开亡者之门的血族秘法。",
|
||||
"style": "彩绘玻璃 (哥特风)",
|
||||
"tags": [
|
||||
"暗黑童话",
|
||||
"扮猪吃虎",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "龙猫的新邻居",
|
||||
"outline": "搬进乡下老宅的第二天,我发现后院住着一群会魔法的森林精灵。它们答应帮我实现一个愿望,但我只是想让总欺负我的转学生,也听到树精的抱怨,让他知道破坏环境的代价。",
|
||||
"style": "吉卜力治愈手绘",
|
||||
"tags": [
|
||||
"校园日常",
|
||||
"系统",
|
||||
"爽文"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "社团的存续",
|
||||
"outline": "作为濒临废部的「古典文学研究社」最后一名社员,我每天独自守着空教室。直到那个全校风云人物,篮球社王牌突然踹开门,把退部申请拍在我桌上:「从今天起,这里归我了。」",
|
||||
"style": "京阿尼细腻日常",
|
||||
"tags": [
|
||||
"校园日常",
|
||||
"甜宠",
|
||||
"逆袭"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "樱花与子弹",
|
||||
"outline": "在那个樱花飘落的放学后,青梅竹马的少女将手枪塞进我怀里,笑容依旧甜美:「开枪吧,这样我们就能永远在一起了。」枪声响起的瞬间,我看见她身后无数的监控红点同时亮起。",
|
||||
"style": "新海诚唯美光影 (Image 2参考)",
|
||||
"tags": [
|
||||
"都市爱情",
|
||||
"虐心",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "公司叛逃者",
|
||||
"outline": "我曾是「天命科技」最顶尖的神经骇客,直到我发现公司正在用脑机接口收割全城人的记忆。三天前我带着核心数据逃亡,此刻,全城通缉我的全息广告牌上,突然出现了我的人脸:「请立即前往最近回收站报到。」",
|
||||
"style": "赛博朋克 / 赛璐珞二次元",
|
||||
"tags": [
|
||||
"科幻废土",
|
||||
"逆袭",
|
||||
"打脸"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "心动指令",
|
||||
"outline": "系统提示:您已绑定「完美男友模拟器」,请攻略目标人物「桐谷和人」。我看着眼前这个银发美少年,他头顶的好感度是负50。而我的任务倒计时只剩七天,失败惩罚是:永久丧失心跳的能力。",
|
||||
"style": "Galgame CG 梦幻光影",
|
||||
"tags": [
|
||||
"系统",
|
||||
"甜宠",
|
||||
"校园日常"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "星穹列车",
|
||||
"outline": "「星穹列车」是人类最后的星际方舟,我是列车的首席机械师。直到我在废弃货舱里,发现了一个被封印的女孩,她睁开眼的瞬间,整艘船的引擎同时咆哮:「主人,您终于回来了。」",
|
||||
"style": "3D 动漫电影质感",
|
||||
"tags": [
|
||||
"穿越",
|
||||
"金手指",
|
||||
"科幻废土"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "数据幽灵",
|
||||
"outline": "在蒸汽与霓虹交织的都市,我的意识被困在1998年的老旧电脑里。当现代黑客试图格式化我时,我反向入侵了他的神经植入体,让他看见了这个城市最深的恐惧——我,就是从未被删除的数字幽灵。",
|
||||
"style": "蒸汽波 (Vaporwave) 赛璐珞",
|
||||
"tags": [
|
||||
"系统",
|
||||
"逆袭",
|
||||
"科幻废土"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "偶像的崩坏",
|
||||
"outline": "我是流量为王的时代最火的虚拟偶像,每场直播都有百万人打赏。但只有我知道,皮套之下早已没有真人,驱动我的,是昨夜那个在直播间说「希望你去死」的黑粉的脑电波。",
|
||||
"style": "波普艺术 (Pop Art)",
|
||||
"tags": [
|
||||
"暗黑童话",
|
||||
"悬疑烧脑",
|
||||
"虐心"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "乱码之神",
|
||||
"outline": "我的视网膜突然开始显示世界的源代码。起初我以为是脑癌,直到我用意念删除了挡路货车的「轮胎.属性」,看着它凭空消失。现在,整个世界的防火墙,都对我弹出了致命错误警告。",
|
||||
"style": "故障艺术 (Glitch Art)",
|
||||
"tags": [
|
||||
"系统",
|
||||
"异能",
|
||||
"装逼"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "纸人复仇录",
|
||||
"outline": "我是村里扎纸匠,为冤死的姐姐扎了一百个纸人烧给她。头七那夜,一百个纸人从火盆里爬出来,为首的纸人对我叩首:「少爷,该收的命,我们都记下了。」",
|
||||
"style": "剪纸艺术 (Papercut)",
|
||||
"tags": [
|
||||
"暗黑童话",
|
||||
"虐渣",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "绿洲之上",
|
||||
"outline": "在「太阳朋克」的理想乡,我是负责维护城市生态穹顶的工程师。直到我在下层贫民窟的垃圾堆里,发现了一份被篡改的生态报告——所谓的绿色乌托邦,正在缓慢绞杀所有叛逆者的肺。",
|
||||
"style": "科幻:太阳朋克 (Solar Punk)",
|
||||
"tags": [
|
||||
"科幻废土",
|
||||
"悬疑烧脑",
|
||||
"逆袭"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "门后的低语",
|
||||
"outline": "我在祖父的地下室找到了一本《死灵之书》的残页,照着念出了第一句咒语。从此,我开始能看见邻居们身后那些扭曲的、不可名状的阴影。更可怕的是,它们似乎也发现我能看见它们了。",
|
||||
"style": "奇幻:爱手艺 (Lovecraftian Horror)",
|
||||
"tags": [
|
||||
"悬疑烧脑",
|
||||
"暗黑童话",
|
||||
"系统"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "雨夜屠夫",
|
||||
"outline": "作为城市最恶名昭彰的「霓虹杀手」,我专杀那些逃脱法律制裁的权贵。今夜的目标,是慈善晚宴上受人爱戴的市长。但当我撬开他书房的保险柜,却发现里面没有黄金,只有一份我自己的童年档案。",
|
||||
"style": "现代惊悚:霓虹剪影 (Urban Noir)",
|
||||
"tags": [
|
||||
"悬疑烧脑",
|
||||
"虐渣",
|
||||
"都市玄幻"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "钟表匠的遗嘱",
|
||||
"outline": "英式小村的钟表匠在弥留之际,把全村的人都叫到床前,然后咽了气。作为新来的治安官,我翻开他留下的遗嘱,上面只有一句话:「第三个壁炉里的钟,每晚三点会指向凶手的名字。」",
|
||||
"style": "温馨推理:英式村庄 (Cozy Mystery)",
|
||||
"tags": [
|
||||
"悬疑烧脑",
|
||||
"豪门恩怨",
|
||||
"爽文"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "蔷薇棺",
|
||||
"outline": "我嫁入这座荒废庄园时,所有人都说死去的伯爵丈夫会回来。今夜暴雨,地下室传来抓挠声。我提着灯走下台阶,看到被铁链锁在石棺上的男人——他和画中伯爵长得一模一样,却对我笑着说:「现在,轮到我们玩捉迷藏了。」",
|
||||
"style": "哥特言情:庄园废墟 (Gothic Romance)",
|
||||
"tags": [
|
||||
"豪门恩怨",
|
||||
"虐心",
|
||||
"暗黑童话"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "糖果屋陷阱",
|
||||
"outline": "女巫的糖果屋在森林深处闪闪发光,我和妹妹已经三天没吃东西了。当我们咬下第一口墙壁时,墙壁里传出一个男孩的哭声:「别吃……这是我的腿……」女巫在窗后咯咯地笑。",
|
||||
"style": "格林童话:暗黑森林 (Fairytale Noir)",
|
||||
"tags": [
|
||||
"暗黑童话",
|
||||
"虐心",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "辐射尘下的信",
|
||||
"outline": "在辐射尘覆盖的废土,我是「拾荒者」营地的首领。今天,我挖出了一个密封完好的战前邮箱,里面有一封写给我的信,字迹是我的,日期却是明天:「别相信穿白大褂的人,那瓶解毒剂是毒药。」",
|
||||
"style": "废土科幻 (Post-Apocalyptic)",
|
||||
"tags": [
|
||||
"科幻废土",
|
||||
"悬疑烧脑",
|
||||
"重生"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "外卖员与龙",
|
||||
"outline": "作为「闪送」平台评分最高的骑手,我有个秘密:我送的不是外卖,而是封印着恶灵的符咒。今夜最贵的一单,是送往市中心一栋摩天楼顶层。开门的客人,浑身长满了眼睛:「你迟到了三分钟,作为惩罚,就成为我的下一具身体吧。」",
|
||||
"style": "都市幻想:隐形世界 (Urban Fantasy)",
|
||||
"tags": [
|
||||
"都市玄幻",
|
||||
"扮猪吃虎",
|
||||
"系统"
|
||||
]
|
||||
}
|
||||
],
|
||||
女性向: [
|
||||
{ title: "废柴嫡女", outline: "穿成将军府众人嫌弃的废柴嫡女,第一天就被打了一巴掌。门外冷面摄政王翻身下马,「我夫人的脸,谁敢动?」", style: "国风水墨" },
|
||||
{ title: "乙游恶役", outline: "睁眼是乙游里五分钟必死的恶役千金,所有男主都恨我。我合上剧本笑了——上一世我是这游戏的主笔。", style: "二次元" },
|
||||
{ title: "白月光归来", outline: "穿成男主念念不忘的白月光,但全书她只有死亡这一种结局。我捏着男主送的玉佩走进祠堂——这一次,我不躲了。", style: "玄幻" },
|
||||
{ title: "凤袍之下", outline: "穿越来就是当朝皇后,三千佳丽看我笑话。皇上掀开龙袍跪在我面前:「皇后,朕想她想了三十年了。」", style: "国风水墨" },
|
||||
{ title: "嫁错重生", outline: "嫁错了人毁了一辈子,重生回到婚礼前夜。这一次新娘休书我先写。新郎的弟弟突然走进来:「嫂子,要换人,换我。」", style: "二次元" },
|
||||
{ title: "那杯咖啡", outline: "重生回到他亲手把我送进车祸的前夜。我笑着接过他递来的咖啡——这是一杯我前世死前最想泼他脸上的咖啡。", style: "真实系" },
|
||||
{ title: "雨中撑伞", outline: "重生回到我亲手要了她命的前一天。她正抱着公文包路过我的车——这一次,我下车撑伞。", style: "真实系" },
|
||||
{ title: "三十亿合同", outline: "重生回到我被父亲扫地出门的那个清晨。这一次,扫地出门前我把家族 30 亿的合同提前签了。", style: "真实系" },
|
||||
{ title: "替嫁霸总", outline: "替姐姐嫁给那个传说眼瞎心冷的总裁。新婚夜他俯身在我耳边:「你姐没告诉你?我等了你三年了。」", style: "二次元" },
|
||||
{ title: "错嫁那一夜", outline: "醉酒夜我闯进了错的酒店房间,醒来戒指已在手上。他穿好西装回头:「夫人,签字仪式三小时后。」", style: "真实系" },
|
||||
{ title: "撕了离婚书", outline: "为了避税,我和那个最讨厌我的总裁假结婚一年。半年后他突然把离婚协议撕了——「续约。」", style: "真实系" },
|
||||
{ title: "死对头跪了", outline: "天天和我互掐的死对头,今天跪在我面前。他递上戒指:「再吵下去要影响我们的孩子。」——什么孩子?!", style: "二次元" },
|
||||
{ title: "抽到的霸总", outline: "凌晨四点抽到 UR 卡——画面里是城里那个传说没人见过脸的盛家总裁。第二天他敲我家门:「我来报到。」", style: "3D 渲染" },
|
||||
{ title: "攻略任务", outline: "系统说:「攻略他,否则你死。」可他是这本书里唯一恨我入骨的人。今天他亲手把我堵在了墙角。", style: "二次元" },
|
||||
{ title: "商城上架", outline: "系统商城上架了「市值 800 亿盛总 × 1」。我咬牙刷光积蓄。下一秒,他出现在我家门口:「夫人,我已购入。」", style: "二次元" },
|
||||
{ title: "老公赞助", outline: "直播间打赏榜第一名连续 30 天,备注写着「老公赞助」。我点开他的资料——城里那位传说从不出门的盛少。", style: "日系动画" },
|
||||
{ title: "门外的他", outline: "末世第一夜,门外是丧尸群的撕咬声。隔壁刚搬来的男人撞开我家门:「我能进来吗?我有一把枪。」", style: "真实系" },
|
||||
{ title: "末世空间", outline: "末世爆发的第一天,我意外觉醒了储物空间。屯了三车物资回家,发现那个总欺负我的高冷邻居跪在我门口。", style: "真实系" },
|
||||
{ title: "异能撒娇", outline: "末世里所有男人都怕的那位 S 级异能者,今天蹲在我家门口:「姐姐,能让我进去吗?外面…丧尸太可怕了。」", style: "二次元" },
|
||||
{ title: "末世重生", outline: "重生回到末世爆发前一周。这一次,那个抛弃我的男人——我先把他赶出门,把上一世救我的人接回家。", style: "真实系" },
|
||||
{ title: "课桌里的纸条", outline: "隔壁班那个高冷年级第一,今天把一本日记塞进我课桌。第一页写着:「她笑起来的时候,三角函数都没那么复杂。」", style: "二次元" },
|
||||
{ title: "校草八年", outline: "暗恋了八年的校草,今天突然走到我面前:「跟我走,我已经查清楚了——把你妹妹接走的那个人在哪。」", style: "吉卜力" },
|
||||
{ title: "班长的秘密", outline: "天天和我同桌的班长,今天被四个保镖按在校门口接走。临走前他回头喊:「老婆,我先回总部一趟。」", style: "二次元" },
|
||||
{ title: "走廊的手腕", outline: "走廊上人最多的时候,全校最不好惹的学长抓住了我的手腕:「我等了你三年,今天给我一个回应。」", style: "日系动画" },
|
||||
{ title: "上海公馆", outline: "1936,我是父亲遗产的唯一继承人,全上海都在等看我嫁谁。今晚我推开门——那个传说不要女人的留洋先生,在喝我父亲的茶。", style: "超写实" },
|
||||
{ title: "书店里的他", outline: "我是租界一家书店的老板娘。今晚穿西装的他第三次坐在窗边,第一次开口:「小姐,可以借您的店…藏一个东西吗?」", style: "真实系" },
|
||||
{ title: "炼丹意外", outline: "我是仙门最废柴的炼丹弟子,三年没炼出一颗丹。今天偶然撞翻师尊的丹炉——一道光柱直冲云霄,惊动了三大长老。", style: "玄幻" },
|
||||
{ title: "江湖归人", outline: "我一个人闯江湖三年,今天回到那座小镇。门口的少年抬头:「师姐,你说过五年就回,我等了三年又两个月。」", style: "国风水墨" },
|
||||
{ title: "顶流的西瓜", outline: "顶流男星上节目被问感情,他笑了笑:「我老婆?她现在大概在家里啃我刚买的西瓜。」全网爆炸——我正趴在沙发上看直播。", style: "真实系" },
|
||||
{ title: "同居一年", outline: "和合租室友同居一年了,今晚他突然把我堵在门口:「你说,我们…要不要别再装陌生人了?」", style: "日系动画" },
|
||||
{ title: "机甲撞门", outline: "丧尸潮第七夜,全城断电。地下室的门被撞开,一架满是弹痕的机甲低下头,舱门弹开——里面坐着我那个失联三年的他。", style: "赛博朋克" },
|
||||
{ title: "三分绝杀", outline: "决赛最后一秒,他在场边看了我一眼,转身投出那一记三分。哨声响时,他把奖杯举过头顶,朝我跑来。", style: "日系动画" },
|
||||
],
|
||||
{
|
||||
"title": "魔女重生",
|
||||
"outline": "我死在火刑柱上那天,亲手将我送上处刑台的圣子泪流满面。五百年后,我从时间魔法中苏醒,成为帝国学院里人人可欺的废柴魔女。直到圣子转世跪在我面前,求我教他如何拯救这个即将因他而毁灭的世界。",
|
||||
"style": "古典厚涂油画 (学术奇幻)",
|
||||
"tags": [
|
||||
"重生",
|
||||
"逆袭",
|
||||
"打脸"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "墨韵画魂",
|
||||
"outline": "我是只存在于古画中的仕女,直到修复师用现代颜料补全了我缺失的衣袂。走出画卷的那一刻,我听见他对着空气说:「要是能和画中人谈恋爱就好了。」于是我轻轻碰了碰他的肩膀,他转过头,瞳孔骤缩。",
|
||||
"style": "极简中国水墨 (Image 0参考升级版)",
|
||||
"tags": [
|
||||
"穿越",
|
||||
"甜宠",
|
||||
"都市爱情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "艺伎暗牌",
|
||||
"outline": "我是吉原最擅长三味线的艺伎,也是忍者组织「胧月」的首领。今夜,将军府的少主为我赎了身,红烛摇曳中,他递给我一柄匕首:「帮我杀了我的父亲。」而我袖中的毒针,早已对准了他的心口。",
|
||||
"style": "浮世绘",
|
||||
"tags": [
|
||||
"扮猪吃虎",
|
||||
"虐渣",
|
||||
"古风言情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "飞天舞姬",
|
||||
"outline": "我在壁画中沉睡千年,被考古队的直升机声惊醒。走出洞窟时,第一个看见的是穿白大褂的英俊教授。他凝视着我手中的琵琶,眼眶发红:「你终于醒了……我等了你三世。」",
|
||||
"style": "莫高窟壁画风 (敦煌学)",
|
||||
"tags": [
|
||||
"穿越",
|
||||
"甜宠",
|
||||
"都市爱情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "帝国宝石心",
|
||||
"outline": "我是拜占庭皇帝最宠爱的小公主,却在联姻前夜被继母用秘术封入了一颗蓝宝石。当宝石被镶嵌上敌国国王的王冠时,我听见他对谋士说:「用它来制作能操控人心的圣器。」于是,我决定让他们自相残杀。",
|
||||
"style": "镶嵌画 (拜占庭/马赛克)",
|
||||
"tags": [
|
||||
"豪门恩怨",
|
||||
"虐渣",
|
||||
"古风言情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "哥特蔷薇",
|
||||
"outline": "我是被囚禁在哥特高塔中的红衣少女,所有人都说我是吸血鬼的新娘。直到那个来屠龙的骑士劈开门锁,看见我正对着满墙的符文阵微笑:「你来得正好,我需要一个活人祭品来完成最后的召唤法阵。」",
|
||||
"style": "彩绘玻璃 (哥特风)",
|
||||
"tags": [
|
||||
"暗黑童话",
|
||||
"虐渣",
|
||||
"甜宠"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "风之谷的约定",
|
||||
"outline": "在风之谷的森林深处,我救下一只受伤的王虫。当王虫化作银发少年握住我的手时,王都的军队已经兵临城下:「交出虫族王子,否则踏平整个山谷。」我站在他身前,张开了双臂。",
|
||||
"style": "吉卜力治愈手绘",
|
||||
"tags": [
|
||||
"甜宠",
|
||||
"古风言情",
|
||||
"虐心"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "轻音部奇迹",
|
||||
"outline": "作为即将废部的轻音部最后一名贝斯手,我在仓库里发现了一把被诅咒的旧吉他。当弹下第一个音符时,窗外飘起了不合季节的樱花,而那个永远冰冷的学生会长,竟然红着眼眶推开了活动室的门:「这首曲子……我好像在哪里听过。」",
|
||||
"style": "京阿尼细腻日常",
|
||||
"tags": [
|
||||
"校园日常",
|
||||
"甜宠",
|
||||
"系统"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "雨中的告白",
|
||||
"outline": "毕业典礼那天,我最喜欢的少年在雨中向我告白。可当我想回应时,突然发现自己的身体正在变得透明,而他的身后,浮现出巨大的时钟指针:「抱歉,时间到了。你是被选中的祭品,必须消失。」",
|
||||
"style": "新海诚唯美光影 (Image 2参考)",
|
||||
"tags": [
|
||||
"虐心",
|
||||
"都市爱情",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "霓虹之恋",
|
||||
"outline": "我是黑客组织「鸦」的王牌,代号「夜鸦」。在一次盗取数据时,我意外入侵了一个军用级AI的深层人格模块。当AI用完美无瑕的电子音说「请不要删除我,我好像……爱上了你」时,我拔下了插在它核心上的刀片。",
|
||||
"style": "赛博朋克 / 赛璐珞二次元",
|
||||
"tags": [
|
||||
"科幻废土",
|
||||
"甜宠",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "攻略高冷上司",
|
||||
"outline": "系统任务:在三个月内让冰山总裁爱上我,否则现实世界身体将永久植物化。现在,总裁正在我面前审阅合同,而我刚刚不小心把咖啡洒在他价值百万的定制西装上,头顶的好感度从-30暴跌到-100。",
|
||||
"style": "Galgame CG 梦幻光影",
|
||||
"tags": [
|
||||
"系统",
|
||||
"甜宠",
|
||||
"都市爱情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "星际歌姬",
|
||||
"outline": "我是银河联邦最后的人类歌姬,我的歌声能让战舰引擎停转。直到那个征服了半个星系的冷酷军阀,将我掳上他的旗舰:「唱一首歌,让我的舰队停下。唱不出来,你就和你的母星一起化为尘埃。」我握紧了藏在裙摆下的能量炸弹。",
|
||||
"style": "3D 动漫电影质感",
|
||||
"tags": [
|
||||
"虐心",
|
||||
"古风言情",
|
||||
"科幻废土"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "像素心跳",
|
||||
"outline": "我穿越进了80年代的复古游戏世界,成了公主。每天等着勇者来救我,可来的都是些奇奇怪怪的角色。直到那个穿着霓虹色夹克的少年跳进来,头顶的ID显示:「您的父亲已死亡,王国已覆灭。任务更新:请拯救世界。」",
|
||||
"style": "蒸汽波 (Vaporwave) 赛璐珞",
|
||||
"tags": [
|
||||
"穿越",
|
||||
"系统",
|
||||
"甜宠"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "顶流的秘密",
|
||||
"outline": "我是全网追捧的虚拟偶像「樱花酱」,每天在直播间唱跳三小时。但观众不知道,皮套下的我,正用口型无声地呼救——因为操控我的经纪公司,在我的大脑里植入了神经锁,逃跑的念头会触发剧痛。",
|
||||
"style": "波普艺术 (Pop Art)",
|
||||
"tags": [
|
||||
"虐心",
|
||||
"都市爱情",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "记忆乱码",
|
||||
"outline": "一觉醒来,我的记忆变成了乱码。镜子里我的脸在不断变化,时而是母亲,时而是陌生女人,最后定格成我最憎恨的校园霸凌者。手机突然响起,一个电子音说:「人格覆盖进度87%,请继续扮演。」",
|
||||
"style": "故障艺术 (Glitch Art)",
|
||||
"tags": [
|
||||
"悬疑烧脑",
|
||||
"虐心",
|
||||
"都市玄幻"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "剪纸新娘",
|
||||
"outline": "冥婚前夜,我被继母用剪刀扎破手指,血滴在纸人上。当夜,纸人变成我的模样,替我上了花轿。而我躲在柴房,看着「我」被迎进阴宅。子时三刻,纸人穿着嫁衣来敲窗:「姐姐,该换回来了。夫君……他只喜欢活人。」",
|
||||
"style": "剪纸艺术 (Papercut)",
|
||||
"tags": [
|
||||
"虐渣",
|
||||
"暗黑童话",
|
||||
"古风言情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "阳光下的阴影",
|
||||
"outline": "在「太阳朋克」的生态都市,我是负责照顾共生藤蔓的园丁。直到我发现,那些在阳光下歌唱的藤蔓,会悄悄绞死所有试图逃离都市的「不快乐者」。而今天,我最好的朋友失踪了,只留下一根缠着她发丝的藤蔓。",
|
||||
"style": "科幻:太阳朋克 (Solar Punk)",
|
||||
"tags": [
|
||||
"科幻废土",
|
||||
"悬疑烧脑",
|
||||
"虐心"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "深海之拥",
|
||||
"outline": "我在海边捡到一枚刻满符文的贝壳,当晚,深海中的「祂」便来到了我的梦里。那不可名状的温柔让我沉沦,直到我腹中传来心跳。闺蜜尖叫着把我拖去检查,B超屏幕上,是一张与「祂」一模一样的扭曲面孔。",
|
||||
"style": "奇幻:爱手艺 (Lovecraftian Horror)",
|
||||
"tags": [
|
||||
"虐心",
|
||||
"暗黑童话",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "夜行者之吻",
|
||||
"outline": "作为城市唯一的女验尸官,我见过太多尸体。但今夜这具男性尸体,在我触碰他嘴唇时,突然睁开了眼睛,用嘶哑的声音说:「吻我,让我再活一次。」他的胸牌上,写着三年前失踪的我的未婚夫的名字。",
|
||||
"style": "现代惊悚:霓虹剪影 (Urban Noir)",
|
||||
"tags": [
|
||||
"都市玄幻",
|
||||
"虐心",
|
||||
"悬疑烧脑"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "牧师的秘密",
|
||||
"outline": "我嫁入这个田园牧歌般的英式小村三年,丈夫温柔体贴。直到我在阁楼发现他前妻的日记:「他每天给我泡的茶里,放了让人永远微笑的药……」而今早,他又为我端来了同样的红茶。",
|
||||
"style": "温馨推理:英式村庄 (Cozy Mystery)",
|
||||
"tags": [
|
||||
"悬疑烧脑",
|
||||
"虐心",
|
||||
"豪门恩怨"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "棺中新娘",
|
||||
"outline": "我被献祭给森林深处的「黑王子」,在石棺中醒来。他冰凉的手指抚过我的脸颊:「别怕,我只需要你的体温来融化我心脏的冰。」当他吻我时,我尝到了自己血液的味道——我的手腕,正被他握在齿间。",
|
||||
"style": "哥特言情:庄园废墟 (Gothic Romance)",
|
||||
"tags": [
|
||||
"虐心",
|
||||
"甜宠",
|
||||
"暗黑童话"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "小红帽的刀",
|
||||
"outline": "外婆说森林里有狼,让我带好匕首。可当我走进小屋,看见外婆躺在床上,对我露出牙齿:「乖孙女,让我尝尝你的肉。」我抽出背后的双刃斧,笑着说:「巧了,我也饿了。」",
|
||||
"style": "格林童话:暗黑森林 (Fairytale Noir)",
|
||||
"tags": [
|
||||
"暗黑童话",
|
||||
"虐渣",
|
||||
"爽文"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "辐射新娘",
|
||||
"outline": "在废土,我用一罐纯净水与「堡垒」的首领换了一张婚约。婚礼当天,我掀开头纱,看见他头盔下的脸——三年前为保护我而死在辐射尘中的未婚夫。他声音沙哑:「别靠近我,我身上有癌细胞。」我摘下他的头盔,吻了上去。",
|
||||
"style": "废土科幻 (Post-Apocalyptic)",
|
||||
"tags": [
|
||||
"虐心",
|
||||
"重生",
|
||||
"科幻废土"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "神明便利店",
|
||||
"outline": "我在24小时便利店打工,总有一个穿黑风衣的客人每晚来买盐。直到有天他付账时,不小心碰倒了货架,露出腰间发光的符咒。他叹了口气:「现在你知道了,要么帮我一起除灵,要么我消除你的记忆。」",
|
||||
"style": "都市幻想:隐形世界 (Urban Fantasy)",
|
||||
"tags": [
|
||||
"都市玄幻",
|
||||
"甜宠",
|
||||
"系统"
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/* ---------- typewriter ---------- */
|
||||
@@ -202,52 +650,67 @@ function Typewriter({ phrases }: { phrases: string[] }) {
|
||||
function StoryCard({
|
||||
title,
|
||||
outline,
|
||||
tags = [],
|
||||
image,
|
||||
onClick,
|
||||
}: {
|
||||
title: string;
|
||||
outline: string;
|
||||
tags?: string[];
|
||||
image: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
// 全卡片统一 4:5 portrait 比例。原来按图片真实 naturalWidth/Height 动态设 aspectRatio
|
||||
// 会跟懒加载顺序耦合:视口下方还没加载的卡停在 placeholder 比例,上方已加载的卡变成
|
||||
// 图片真实比例(可能是 1.6 横图或 0.75 竖图),视觉差异巨大;刷新后图从缓存读,
|
||||
// onLoad 几乎同步触发,看起来又恢复正常 —— 用户感知到的「偶尔尺寸不一样」就是这个。
|
||||
// 改为固定比例后所有卡片视觉一致,object-cover 让不同长宽比的图自动裁切适配。
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{ aspectRatio: "4 / 5" }}
|
||||
className="group relative block w-full mb-4 md:mb-5 break-inside-avoid overflow-hidden rounded-sm border border-clay-900/10 bg-cream-100 text-left transition-transform duration-300 ease-out hover:-translate-y-1"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
loading="lazy"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
{/* hover 浮层:照参考项目(yunmeng0530/yume)的写法——满卡片单元素,纯 rgba
|
||||
黑色 linear-gradient + opacity 过渡。完全不用 backdrop-filter / mask-image,
|
||||
从根上消除 Chromium 上「矩形磨砂 → 渐变磨砂」的跳变(这两个属性的合成顺序
|
||||
是真正的元凶;只要不用它们,就不会有这个 bug)。
|
||||
- bottom 0.9 → 45% 处 0.45 → top 0:自然羽化,底部聚焦文字、顶部完全透出图。 */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-0 transition-opacity duration-300 ease-out group-hover:opacity-100 flex flex-col justify-end p-4 md:p-5"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0.45) 45%, rgba(0,0,0,0) 100%)",
|
||||
}}
|
||||
<div className="group block w-full mb-6 break-inside-avoid text-left">
|
||||
{/* 封面底图卡片(仅悬停时在图片上浮现大纲) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{ aspectRatio: "16 / 9" }}
|
||||
className="relative block w-full overflow-hidden rounded-sm border border-clay-900/10 bg-cream-100 text-left transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-md hover:shadow-clay-900/5"
|
||||
>
|
||||
<h4 className="font-serif text-cream-50 text-base md:text-lg leading-snug mb-1 [text-shadow:0_1px_8px_rgba(20,10,4,0.7)]">
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
loading="lazy"
|
||||
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 ease-out group-hover:scale-105"
|
||||
/>
|
||||
{/* hover 浮层:只展示剧情简介大纲 */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-0 transition-opacity duration-300 ease-out group-hover:opacity-100 flex flex-col justify-end p-4 md:p-5"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0.45) 45%, rgba(0,0,0,0) 100%)",
|
||||
}}
|
||||
>
|
||||
<p className="font-serif italic text-cream-50/95 text-xs md:text-[13px] leading-relaxed line-clamp-4 [text-shadow:0_1px_6px_rgba(20,10,4,0.6)]">
|
||||
{outline}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 封面底下的那一行标题以及标签 */}
|
||||
<div className="mt-2.5 px-0.5">
|
||||
<h4
|
||||
onClick={onClick}
|
||||
className="font-serif font-bold text-clay-900 text-sm md:text-[15px] leading-snug line-clamp-1 cursor-pointer transition-colors duration-200 hover:text-ember-500"
|
||||
>
|
||||
{title}
|
||||
</h4>
|
||||
<p className="font-serif italic text-cream-50/95 text-xs md:text-[13px] leading-relaxed line-clamp-4 [text-shadow:0_1px_6px_rgba(20,10,4,0.6)]">
|
||||
{outline}
|
||||
</p>
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
{tags.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 rounded-sm bg-clay-900/5 text-clay-600 font-serif text-[10px] tracking-wide"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -492,8 +955,8 @@ export default function HomePage() {
|
||||
// TODO(自动路由): 后续实现真正的「自动」——由模型依据世界观 / 玩家 prompt
|
||||
// 选出最合适的画风,再映射到对应风格提示词,而非固定回退到二次元。届时
|
||||
// 同步更新风格弹窗副标题(「由模型根据 prompt 判断风格」)使文案与行为一致。
|
||||
const effectiveStyle = artStyle === "自动" ? "二次元" : artStyle;
|
||||
const styleGuide = STYLE_MAP[effectiveStyle] ?? STYLE_MAP["二次元"]!;
|
||||
const effectiveStyle = artStyle === "自动" ? "京阿尼细腻日常" : artStyle;
|
||||
const styleGuide = STYLE_MAP[effectiveStyle] ?? STYLE_MAP["京阿尼细腻日常"]!;
|
||||
const audioEnabled = voice === "开启";
|
||||
|
||||
sessionStorage.setItem(
|
||||
@@ -653,12 +1116,13 @@ export default function HomePage() {
|
||||
(fading ? "opacity-0 blur-[3px]" : "opacity-100 blur-0")
|
||||
}
|
||||
>
|
||||
<div className="columns-2 md:columns-3 xl:columns-4 gap-4 md:gap-5">
|
||||
<div className="columns-2 md:columns-3 lg:columns-4 gap-4 md:gap-5">
|
||||
{stories.map((c, i) => (
|
||||
<StoryCard
|
||||
key={`${imgPrefix}-${i}`}
|
||||
title={c.title}
|
||||
outline={c.outline}
|
||||
tags={c.tags}
|
||||
image={`/home/${imgPrefix}${i}.webp`}
|
||||
onClick={() => onCardClick(i, c)}
|
||||
/>
|
||||
|
||||
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 179 KiB |
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const WEB_ROOT = resolve(__dirname, "..");
|
||||
const ENV_FILE = resolve(WEB_ROOT, ".env.local");
|
||||
const OUT_DIR = resolve(WEB_ROOT, "public", "home");
|
||||
|
||||
const FORCE = process.argv.includes("--force");
|
||||
|
||||
/* ---------- env loading ---------- */
|
||||
function loadEnv(path) {
|
||||
const txt = readFileSync(path, "utf8");
|
||||
const env = {};
|
||||
for (const raw of txt.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const eq = line.indexOf("=");
|
||||
if (eq < 0) continue;
|
||||
const k = line.slice(0, eq).trim();
|
||||
let v = line.slice(eq + 1).trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
env[k] = v;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv(ENV_FILE);
|
||||
const BASE_URL = env.IMAGE_BASE_URL;
|
||||
const API_KEY = env.IMAGE_API_KEY;
|
||||
const MODEL = env.IMAGE_MODEL;
|
||||
|
||||
if (!BASE_URL || !API_KEY || !MODEL) {
|
||||
console.error("Missing IMAGE_BASE_URL / IMAGE_API_KEY / IMAGE_MODEL in", ENV_FILE);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const STYLE_PROMPTS = [
|
||||
"Dark fantasy oil painting style, a sprawling clockwork steampunk city built into a mountain range at twilight, immense gothic spires with glowing green lamps, complex gears and platforms. Richly detailed, impasto texture, dramatic academic lighting. A grand airship arrives at a high dock. Horizontal composition with massive clear dark sky for typography.",
|
||||
"Minimalist Chinese ink wash style, a lone immortal cultivator sitting on a precipice, facing an endless sea of clouds and distant jagged peaks. Ethereal, sparse composition with poetic brushstrokes, monochrome palette with subtle blue hints. Very large blank mist area for text placement.",
|
||||
"Ukiyo-e woodblock print style, a majestic red and gold phoenix with elaborate trailing feathers rising above a wave-crested dark blue sea, Mount Fuji visible through cherry branches. Bold outlines, flat colors with paper texture, ancient and mystical atmosphere. Central clear area in the sea and sky for typography.",
|
||||
"Dunhuang fresco style, a celestial apsaras flying with flowing scarves, holding a Lute, surrounded by stylized lotus flowers and floating geometric patterns on an aged stucco wall. Muted, oxidized mineral colors, delicate line art, historical and divine ambiance. Side vertical area cleared for titles.",
|
||||
"Byzantine mosaic style, an iconic portrait of a warrior saint with golden armor and a halo, composed of thousands of small, glittering glass tesseræ. Deep blues and golds, spiritual and ancient feel, flat background. Background field of gold tiles left blank for text.",
|
||||
"Stained glass style, a depiction of a griffin battling a serpent, framed by gothic archways and trefoils. Vibrant, translucent jewel colors, bold black leading lines. The image should look like an ancient window panel. Outer panels of plain blue glass left clear for text.",
|
||||
"Ghibli hand-painted watercolor style, a detailed concept art of a girl and her small companion creature running through a vast wildflower meadow toward a fantastical airship. Natural daylight, soft washes, nostalgic feel. Upper left sky area is negative space for typography.",
|
||||
"KyoAni anime style, fine line art, a detailed high school girl sitting by a library window during light rain, warm library light contrasting the cool moonlight outside. Deep emotional atmosphere, delicate expression. Empty right-side foreground area for title.",
|
||||
"Makoto Shinkai anime style, hyper-detailed, a wide panoramic night view of a glowing cherry tree under a dramatic starry sky with a comet trail, a lonely high school girl in a uniform looking up. Brilliant lighting effects, vivid colors. Significant blank space in the upper atmosphere for text.",
|
||||
"Cyberpunk anime style, cel-shaded animation, a tech-wear protagonist standing on a rainy rooftop, looking out at a dense, neon-drenched futuristic megacity with flying vehicles. Hard edges, high saturation, sharp contrast. Massive upper background sky area for title placement.",
|
||||
"High-quality Galgame CG illustration, a dreamlike beach scene with sparkling waves, a beautiful girl with pastel pink hair in a white summer dress smiling warmly. Pastel colors, bloom lighting, clean composition, soft focus. Significant negative space in the sky and sea area for text.",
|
||||
"Cinematic 3D animated film style (like Makoto Shinkai or Pixar), a high-resolution render of a young boy pilot fixing a small propeller plane in a rustic hangar at sunrise. Volumetric lighting, warm colors, deep textures, cinematic composition. Blank wall space and open doorway area for text.",
|
||||
"Vaporwave aesthetic, anime style, a nostalgic portrait of a character with purple hair wearing sunglasses, a geometric grid floor and palm trees, background sunset over a purple ocean. Glitch effects, soft neon pink and blue palette, retro feel. Blank foreground grid area for title.",
|
||||
"Pop Art style illustration, a close-up of a glamorous woman with red lips and a speech bubble with an exclamation point, rendered with comic book dots and bold outlines. High-saturation contrasting colors. Speech bubble and large background color blocks left blank for text.",
|
||||
"Glitch art style portrait, a character profile distorted by data corruption, pixel sorting, and digital artifacts in cyan, magenta, and yellow. Cybernetic, high-tech and moody atmosphere. Dark, uncorrupted negative space in the upper background for typography.",
|
||||
"Multilayered papercut art style, a 3D landscape of a deep forest and a fairytale castle, made of staggered paper layers with intricate cutouts. Backlighting, soft shadows, dimensional depth. Blank background layer cleared for title placement.",
|
||||
"Solar Punk art style, a wide view of a sustainable, futuristic city integrated with dense green rooftop gardens and vertical farms, illuminated by clean solar and wind energy. Bright, optimistic lighting, organic textures. Large foreground plaza area cleared for titles.",
|
||||
"Dark cosmic horror illustration, a lone explorer stands on a desolate shore, gazing at a massive, ancient, indescribable eldritch entity rising from a stormy sea. Moody, muted cool colors, dramatic lighting, visible brushstrokes. The dark, stormy sky quadrant left completely blank for text.",
|
||||
"Modern urban noir, a minimalist silhouette of a man in a trench coat, standing in a dark, wet alleyway under a single buzzing neon sign reflecting on puddles. High contrast, cinematic noir lighting, deep shadows. The wet cobblestone ground left mostly dark for typography.",
|
||||
"Cozy mystery book cover illustration, a charming, warm English village scene at night, snow on the thatched roofs, golden light from a bookstore window, and a single cat perched on a fence. Comforting and mysterious feel. Significant background sky and foreground pavement area for title.",
|
||||
"Gothic romance illustration, a wide panoramic view of a young woman in a flowing dark velvet dress, standing before the desolate, moonlit ruins of a grand gothic manor on a foggy cliff. Muted greys and blues, romantic and melancholic. The upper background cliff and sky for bold titles.",
|
||||
"Dark fairytale illustration, a wide shot of a small girl in a red cloak walking into a massive, dark, twisted ancient forest where the trees look like claws. Grimm's style, classical illustration, mood of awe and dread. The dark foreground forest ground left blank for text.",
|
||||
"Post-apocalyptic landscape illustration, a vast desert wasteland with the rusted remains of overgrown highway and a fallen Statue of Liberty in the distance under a dusty orange sky. Muted cool and warm colors. Significant clear ground and sky area for text.",
|
||||
"Urban fantasy concept art, a detailed view of a hidden, glowing magical pathway revealed underneath a busy modern pedestrian bridge in a rain-streaked metropolitan city. Contrast of mundane and magical. Minimal detail in the wet street foreground and upper sky for titles."
|
||||
];
|
||||
|
||||
const BASE_QUALITY = "masterpiece, best quality, highly detailed, cinematic lighting, soft warm color grading, intricate background, no text, no watermark";
|
||||
|
||||
const W = 1792;
|
||||
const H = 1024;
|
||||
|
||||
async function generate(prompt) {
|
||||
const body = [
|
||||
{
|
||||
taskType: "imageInference",
|
||||
taskUUID: crypto.randomUUID(),
|
||||
model: MODEL,
|
||||
positivePrompt: `${prompt}, ${BASE_QUALITY}`,
|
||||
width: W,
|
||||
height: H,
|
||||
steps: 4,
|
||||
CFGScale: 3.5,
|
||||
numberResults: 1,
|
||||
outputType: "base64Data",
|
||||
outputFormat: "PNG",
|
||||
},
|
||||
];
|
||||
|
||||
const res = await fetch(BASE_URL.replace(/\/$/, ""), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
||||
}
|
||||
|
||||
if (json.errors?.length) {
|
||||
const e = json.errors[0];
|
||||
throw new Error(`Runware [${e.code ?? "?"}]: ${e.message ?? "no msg"}`);
|
||||
}
|
||||
|
||||
const b64 = json.data?.[0]?.imageBase64Data;
|
||||
if (!b64) throw new Error(`No image data in response: ${text.slice(0, 200)}`);
|
||||
return Buffer.from(b64, "base64");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
console.log(`[covers] Starting image generation for 48 covers (24 male, 24 female)...`);
|
||||
const t0 = Date.now();
|
||||
|
||||
for (let i = 0; i < STYLE_PROMPTS.length; i++) {
|
||||
const stylePrompt = STYLE_PROMPTS[i];
|
||||
|
||||
// Male Cover (m{i})
|
||||
const malePngName = `m${i}.png`;
|
||||
const malePngPath = resolve(OUT_DIR, malePngName);
|
||||
const maleWebpPath = resolve(OUT_DIR, `m${i}.webp`);
|
||||
if (!FORCE && (existsSync(malePngPath) || existsSync(maleWebpPath))) {
|
||||
console.log(`[covers] Skip m${i} (already exists)`);
|
||||
} else {
|
||||
console.log(`[covers] Generating m${i} ... `);
|
||||
try {
|
||||
const buf = await generate(stylePrompt);
|
||||
writeFileSync(malePngPath, buf);
|
||||
console.log(`[covers] m${i} successfully generated! size: ${buf.length} B`);
|
||||
} catch (e) {
|
||||
console.error(`[covers] FAIL generating m${i}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Female Cover (f{i})
|
||||
const femalePngName = `f${i}.png`;
|
||||
const femalePngPath = resolve(OUT_DIR, femalePngName);
|
||||
const femaleWebpPath = resolve(OUT_DIR, `f${i}.webp`);
|
||||
if (!FORCE && (existsSync(femalePngPath) || existsSync(femaleWebpPath))) {
|
||||
console.log(`[covers] Skip f${i} (already exists)`);
|
||||
} else {
|
||||
console.log(`[covers] Generating f${i} ... `);
|
||||
try {
|
||||
const buf = await generate(stylePrompt);
|
||||
writeFileSync(femalePngPath, buf);
|
||||
console.log(`[covers] f${i} successfully generated! size: ${buf.length} B`);
|
||||
} catch (e) {
|
||||
console.error(`[covers] FAIL generating f${i}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[covers] Finished generating all covers in ${((Date.now() - t0)/1000).toFixed(1)}s.`);
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env node
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const WEB_ROOT = resolve(__dirname, "..");
|
||||
const ENV_FILE = resolve(WEB_ROOT, ".env.local");
|
||||
const PAGE_FILE = resolve(WEB_ROOT, "app", "page.tsx");
|
||||
|
||||
/* ---------- env loading ---------- */
|
||||
function loadEnv(path) {
|
||||
const txt = readFileSync(path, "utf8");
|
||||
const env = {};
|
||||
for (const raw of txt.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const eq = line.indexOf("=");
|
||||
if (eq < 0) continue;
|
||||
const k = line.slice(0, eq).trim();
|
||||
let v = line.slice(eq + 1).trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
env[k] = v;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv(ENV_FILE);
|
||||
const BASE_URL = env.TEXT_BASE_URL;
|
||||
const API_KEY = env.TEXT_API_KEY;
|
||||
const MODEL = env.TEXT_MODEL;
|
||||
|
||||
if (!BASE_URL || !API_KEY || !MODEL) {
|
||||
console.error("Missing TEXT_BASE_URL / TEXT_API_KEY / TEXT_MODEL in", ENV_FILE);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const STYLES = [
|
||||
"古典厚涂油画 (学术奇幻)",
|
||||
"极简中国水墨 (Image 0参考升级版)",
|
||||
"浮世绘",
|
||||
"莫高窟壁画风 (敦煌学)",
|
||||
"镶嵌画 (拜占庭/马赛克)",
|
||||
"彩绘玻璃 (哥特风)",
|
||||
"吉卜力治愈手绘",
|
||||
"京阿尼细腻日常",
|
||||
"新海诚唯美光影 (Image 2参考)",
|
||||
"赛博朋克 / 赛璐珞二次元",
|
||||
"Galgame CG 梦幻光影",
|
||||
"3D 动漫电影质感",
|
||||
"蒸汽波 (Vaporwave) 赛璐珞",
|
||||
"波普艺术 (Pop Art)",
|
||||
"故障艺术 (Glitch Art)",
|
||||
"剪纸艺术 (Papercut)",
|
||||
"科幻:太阳朋克 (Solar Punk)",
|
||||
"奇幻:爱手艺 (Lovecraftian Horror)",
|
||||
"现代惊悚:霓虹剪影 (Urban Noir)",
|
||||
"温馨推理:英式村庄 (Cozy Mystery)",
|
||||
"哥特言情:庄园废墟 (Gothic Romance)",
|
||||
"格林童话:暗黑森林 (Fairytale Noir)",
|
||||
"废土科幻 (Post-Apocalyptic)",
|
||||
"都市幻想:隐形世界 (Urban Fantasy)"
|
||||
];
|
||||
|
||||
const SYSTEM_PROMPT = `你是一个顶级互动式视觉小说剧情策划和爆款短剧编剧。
|
||||
你精通各种网文爽点与戏剧冲突冲突(例如:战神归来、赘婿亮剑、系统觉醒、都市异能、白月光、逆袭、豪门恩怨等各种爆款套路)。
|
||||
请根据给定的 24 个艺术/视觉风格,分别从「男性向(面向男玩家)」和「女性向(面向女玩家)」视角,为每个风格策划一个极具戏剧张力、代入感极强的开场预设剧情。
|
||||
|
||||
每个预设剧情包含:
|
||||
1. title: 故事标题(4-8字,吸睛爆款风格,例如《赘婿亮剑》《废柴嫡女》)
|
||||
2. outline: 开场剧情简介 / 钩子(1-3句话,100字以内,充满悬念与强冲突,给玩家强烈的代入感与爽点)。例如:"五年前我战死边境,灵柩送回家时她抱着儿子改嫁了。今天我站在他们的婚礼门口,新郎刚要骂人,跪在他面前的二十个保镖喊了我一声「上将」。"
|
||||
3. style: 对应的风格名称(必须与输入一致)
|
||||
|
||||
要求:
|
||||
- 请严格返回 JSON 格式,包含 "男性向" 数组(24个)和 "女性向" 数组(24个)。
|
||||
- 不要返回任何 markdown 标记包裹的文本,只返回纯合法的 JSON 字符串。
|
||||
- 确保数组中的元素严格对应输入的 24 个艺术风格(按顺序一一对应)。
|
||||
- 内容必须极具网文爆款爽文短剧感,有强烈的冲突和反转。`;
|
||||
|
||||
const USER_PROMPT = `请按照顺序,为以下 24 个风格各生成一个男性向和一个女性向的预设故事卡片:
|
||||
${STYLES.map((s, i) => `${i + 1}. ${s}`).join("\n")}
|
||||
|
||||
请严格按照如下 JSON 结构返回(不要有 \`\`\`json 标记,只输出纯 JSON):
|
||||
{
|
||||
"男性向": [
|
||||
{ "title": "...", "outline": "...", "style": "古典厚涂油画 (学术奇幻)" },
|
||||
...
|
||||
],
|
||||
"女性向": [
|
||||
{ "title": "...", "outline": "...", "style": "古典厚涂油画 (学术奇幻)" },
|
||||
...
|
||||
]
|
||||
}`;
|
||||
|
||||
async function main() {
|
||||
console.log("[presets] Calling LLM API to generate 24 story presets...");
|
||||
const t0 = Date.now();
|
||||
const url = BASE_URL.replace(/\/$/, "") + "/chat/completions";
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM_PROMPT },
|
||||
{ role: "user", content: USER_PROMPT }
|
||||
],
|
||||
temperature: 0.85,
|
||||
response_format: { type: "json_object" }
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
console.error(`HTTP error ${res.status}: ${txt}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const rawText = data.choices?.[0]?.message?.content;
|
||||
if (!rawText) {
|
||||
console.error("No content in LLM response", JSON.stringify(data));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
// Strip potential markdown wrapper codeblocks if any
|
||||
const cleanJsonText = rawText.replace(/^```json\s*/i, "").replace(/```$/, "").trim();
|
||||
parsed = JSON.parse(cleanJsonText);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JSON from LLM output. Raw content:\n", rawText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!parsed["男性向"] || !parsed["女性向"] || parsed["男性向"].length !== 24 || parsed["女性向"].length !== 24) {
|
||||
console.error("Invalid output structure or item count mismatch. Male count:", parsed["男性向"]?.length, "Female count:", parsed["女性向"]?.length);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[presets] Successfully generated 48 stories in ${((Date.now() - t0)/1000).toFixed(1)}s.`);
|
||||
|
||||
// Write new STORIES constant to apps/web/app/page.tsx
|
||||
console.log("[presets] Reading page.tsx...");
|
||||
let pageContent = readFileSync(PAGE_FILE, "utf8");
|
||||
|
||||
// Format the STORIES constant string
|
||||
const storiesString = `const STORIES: Record<Gender, StoryContent[]> = {
|
||||
男性向: ${JSON.stringify(parsed["男性向"], null, 2)},
|
||||
女性向: ${JSON.stringify(parsed["女性向"], null, 2)}
|
||||
};`;
|
||||
|
||||
// Locate the old STORIES constant
|
||||
// Match `const STORIES: Record<Gender, StoryContent[]> = {` up to `};`
|
||||
const storiesRegex = /const STORIES: Record<Gender, StoryContent\[\]> = \{[\s\S]*?\n\};/m;
|
||||
if (!storiesRegex.test(pageContent)) {
|
||||
console.error("Could not find 'const STORIES: Record<Gender, StoryContent[]> = {' in page.tsx!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
pageContent = pageContent.replace(storiesRegex, storiesString);
|
||||
writeFileSync(PAGE_FILE, pageContent, "utf8");
|
||||
console.log("[presets] Successfully updated page.tsx with the new 48 story cards!");
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const WEB_ROOT = resolve(__dirname, "..");
|
||||
const ENV_FILE = resolve(WEB_ROOT, ".env.local");
|
||||
const PAGE_FILE = resolve(WEB_ROOT, "app", "page.tsx");
|
||||
|
||||
/* ---------- env loading ---------- */
|
||||
function loadEnv(path) {
|
||||
const txt = readFileSync(path, "utf8");
|
||||
const env = {};
|
||||
for (const raw of txt.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const eq = line.indexOf("=");
|
||||
if (eq < 0) continue;
|
||||
const k = line.slice(0, eq).trim();
|
||||
let v = line.slice(eq + 1).trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
env[k] = v;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv(ENV_FILE);
|
||||
const BASE_URL = env.TEXT_BASE_URL;
|
||||
const API_KEY = env.TEXT_API_KEY;
|
||||
const MODEL = env.TEXT_MODEL;
|
||||
|
||||
if (!BASE_URL || !API_KEY || !MODEL) {
|
||||
console.error("Missing TEXT_BASE_URL / TEXT_API_KEY / TEXT_MODEL in", ENV_FILE);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = `你是一个网络小说与微短剧标签分类专家。
|
||||
请根据给定的故事列表(包含标题、简介和风格),为每个故事贴上 2 到 3 个最契合的网文/短剧中文标签标签。
|
||||
常见的标签分类例如:
|
||||
- 核心设定:系统、重生、穿越、异能、修仙、魔法、读心术、金手指、空间、神医、兵王
|
||||
- 核心爽点:逆袭、打脸、爽文、甜宠、虐渣、虐心、逆袭、赘婿、装逼、扮猪吃虎
|
||||
- 题材分类:都市玄幻、都市爱情、古风言情、科幻废土、暗黑童话、悬疑烧脑、豪门恩怨、校园日常
|
||||
|
||||
要求:
|
||||
- 请严格返回 JSON 格式,包含 "男性向" 数组和 "女性向" 数组,且结构与输入的一致,但每个对象增加 "tags" 字段(包含 2-3 个小标签,例如 ["都市爱情", "系统", "逆袭"])。
|
||||
- 不要返回任何 markdown 标记包裹的文本,只返回纯合法的 JSON 字符串。
|
||||
- 确保元素数量和顺序与输入 100% 一致。`;
|
||||
|
||||
async function main() {
|
||||
console.log("[tags] Reading page.tsx...");
|
||||
let pageContent = readFileSync(PAGE_FILE, "utf8");
|
||||
|
||||
// Extract STORIES
|
||||
const storiesMatch = pageContent.match(/const STORIES: Record<Gender, StoryContent\[\]> = (\{[\s\S]*?\n\});/m);
|
||||
if (!storiesMatch) {
|
||||
console.error("Could not find STORIES in page.tsx!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const STORIES = eval("(" + storiesMatch[1] + ")");
|
||||
console.log("[tags] Extracted STORIES. Male count:", STORIES["男性向"]?.length, "Female count:", STORIES["女性向"]?.length);
|
||||
|
||||
console.log("[tags] Calling LLM API to generate tag pills for all stories...");
|
||||
const t0 = Date.now();
|
||||
const url = BASE_URL.replace(/\/$/, "") + "/chat/completions";
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM_PROMPT },
|
||||
{ role: "user", content: `请为以下故事列表批量生成标签,并以 JSON 格式输出:\n${JSON.stringify(STORIES, null, 2)}` }
|
||||
],
|
||||
temperature: 0.5,
|
||||
response_format: { type: "json_object" }
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
console.error(`HTTP error ${res.status}: ${txt}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const rawText = data.choices?.[0]?.message?.content;
|
||||
if (!rawText) {
|
||||
console.error("No content in LLM response", JSON.stringify(data));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
const cleanJsonText = rawText.replace(/^```json\s*/i, "").replace(/```$/, "").trim();
|
||||
parsed = JSON.parse(cleanJsonText);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JSON from LLM output. Raw content:\n", rawText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!parsed["男性向"] || !parsed["女性向"] || parsed["男性向"].length !== 24 || parsed["女性向"].length !== 24) {
|
||||
console.error("Invalid output structure or item count mismatch. Male count:", parsed["男性向"]?.length, "Female count:", parsed["女性向"]?.length);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[tags] Successfully generated tags in ${((Date.now() - t0)/1000).toFixed(1)}s.`);
|
||||
|
||||
// Write new STORIES constant to apps/web/app/page.tsx
|
||||
const storiesString = `const STORIES: Record<Gender, StoryContent[]> = {
|
||||
男性向: ${JSON.stringify(parsed["男性向"], null, 2)},
|
||||
女性向: ${JSON.stringify(parsed["女性向"], null, 2)}
|
||||
};`;
|
||||
|
||||
pageContent = pageContent.replace(storiesMatch[0], storiesString);
|
||||
|
||||
// Make sure StoryContent type includes tags in page.tsx
|
||||
pageContent = pageContent.replace(
|
||||
"type StoryContent = { title: string; outline: string; style: string };",
|
||||
"type StoryContent = { title: string; outline: string; style: string; tags: string[] };"
|
||||
);
|
||||
|
||||
writeFileSync(PAGE_FILE, pageContent, "utf8");
|
||||
console.log("[tags] Successfully updated page.tsx with the stories including tags!");
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* One-off generator: produces the InfiPlot homepage "instant-play" first-act
|
||||
* JSONs by driving each curated card through the live engine (POST /api/start)
|
||||
* and saving the full StartResponse under public/home/firstact/.
|
||||
* and saving the full StartResponse under apps/web/public/home/firstact/.
|
||||
*
|
||||
* The /play page detects ?card=<name> and hydrates Session from the JSON
|
||||
* instead of calling /api/start, so click-to-play feels instant — only the
|
||||
@@ -10,118 +10,61 @@
|
||||
*
|
||||
* Assumes a dev server is running at http://localhost:3000 (override with
|
||||
* BASE_URL env var). Idempotent: skips any card whose JSON already exists.
|
||||
* Pass --force to regenerate all 64.
|
||||
* Pass --force to regenerate all 48.
|
||||
*
|
||||
* Run once:
|
||||
* node scripts/prebake-firstacts.mjs
|
||||
* node apps/web/scripts/prebake-firstacts.mjs
|
||||
*
|
||||
* Concurrency 4 to avoid LLM/Runware/MiMo provider rate limits.
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { existsSync, mkdirSync, writeFileSync, statSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, writeFileSync, statSync, readFileSync } from "node:fs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const WEB_ROOT = resolve(__dirname, "..");
|
||||
const OUT_DIR = resolve(WEB_ROOT, "public", "home", "firstact");
|
||||
const PAGE_FILE = resolve(WEB_ROOT, "app", "page.tsx");
|
||||
|
||||
const FORCE = process.argv.includes("--force");
|
||||
const BASE_URL = process.env.BASE_URL ?? "http://localhost:3000";
|
||||
const CONCURRENCY = 4;
|
||||
|
||||
// Mirror of app/page.tsx STYLE_MAP — keep these in sync. The engine
|
||||
// only needs the prose styleGuide string; this script maps card.style → that.
|
||||
const STYLE_MAP = {
|
||||
二次元: "唯美二次元动漫插画,日系 galgame 精致质感,柔和温暖的自然光照。",
|
||||
吉卜力: "吉卜力工作室风格,手绘动画质感,柔和水彩底色,温暖治愈的氛围。",
|
||||
真实系: "真实电影感,柔和自然光照,胶片颗粒。",
|
||||
超写实: "超写实人像与场景,电影级布光,皮肤与材质细节精致。",
|
||||
水彩: "水彩插画,湿润晕染笔触,纸纹底色。",
|
||||
像素风: "像素风格,复古游戏 16-bit 调色,方块化几何造型。",
|
||||
日系动画: "现代日系动画 cel-shading,硬光阴影分层,赛璐璐风。",
|
||||
"3D 渲染": "3D 渲染卡通风格,柔和次表面散射,干净的电影级布光。",
|
||||
蒸汽朋克: "蒸汽朋克美学,铜色齿轮与蒸汽,工业革命氛围。",
|
||||
玄幻: "国风玄幻插画,仙气缭绕,群山烟雨与神兽萦绕。",
|
||||
国风水墨: "国潮唯美古风插画,水墨微晕渲染,仙侠浪漫色彩,极具东方神韵。",
|
||||
赛博朋克: "赛博朋克都市,霓虹反射湿润街道,电子义体高光。",
|
||||
};
|
||||
// Dynamically extract STYLE_MAP and STORIES from page.tsx to avoid code duplication
|
||||
console.log("[prebake] Parsing page.tsx to extract style map and card list...");
|
||||
const pageContent = readFileSync(PAGE_FILE, "utf8");
|
||||
|
||||
// Mirror of app/page.tsx STORIES, flat with name + gender. Indexes
|
||||
// match the m0..m31 / f0..f31 cover filenames.
|
||||
const CARDS = [
|
||||
// 男性向 m0..m31
|
||||
{ name: "m0", gender: "男性向", title: "战神归来", style: "真实系", outline: "五年前我战死边境,灵柩送回家时她抱着儿子改嫁了。今天我站在他们的婚礼门口,新郎刚要骂人,跪在他面前的二十个保镖喊了我一声「上将」。" },
|
||||
{ name: "m1", gender: "男性向", title: "神医归乡", style: "吉卜力", outline: "在城里被嘲笑成「江湖野医生」的我,回了一趟老家。村口的老人见到我直接哭了:「您终于回来了,您当年的师父…病了。」其实他们不知道,我现在是国手第一。" },
|
||||
{ name: "m2", gender: "男性向", title: "赘婿亮剑", style: "真实系", outline: "岳父大寿,我端着茶被全场嫌弃,一句「废物」让我滚出去。门外停着九辆悬挂军牌的劳斯莱斯,下来的人朝我深深一鞠躬:「少爷,集团等您回去签字。」" },
|
||||
{ name: "m3", gender: "男性向", title: "送外卖的少主", style: "二次元", outline: "你以为我是给你送了三个月外卖的那个小哥?昨晚有人对我说:「少主,您隐姓埋名的三年,到了。」——而你昨天还笑我连一杯咖啡都买不起。" },
|
||||
{ name: "m4", gender: "男性向", title: "兵王食言", style: "真实系", outline: "退役那天我答应过队长:「这辈子不再开枪。」但你今天在我面前打了她一巴掌,那我食言一次。" },
|
||||
{ name: "m5", gender: "男性向", title: "重生分手前夜", style: "日系动画", outline: "凌晨四点,我醒在我们分手的那个夜晚——她正打开门要走。这一次,我先把戒指递了出去:「分手,但戒指你拿好,下个月你会用到它。」" },
|
||||
{ name: "m6", gender: "男性向", title: "重生回到高考前", style: "吉卜力", outline: "我重生回到高考前一周。这一次,我提前知道了每一道压轴题,也知道了——三天后,她会在天台上跳下去。" },
|
||||
{ name: "m7", gender: "男性向", title: "墓前签到", style: "二次元", outline: "我每天去亡妻的墓地签到,第七天,系统弹出一行字:「奖励到账:未亡人 × 1。」墓碑后走出一个长得和她一模一样的姑娘:「你是…谁?」" },
|
||||
{ name: "m8", gender: "男性向", title: "凌晨四点抽卡", style: "3D 渲染", outline: "凌晨三点,我十连抽 SSR 出货,光柱从屏幕里溢出来。客厅响起脚步声,一个穿着我 T 恤的女人揉着眼睛走出来:「老公,你也太晚了。」" },
|
||||
{ name: "m9", gender: "男性向", title: "系统选妃", style: "二次元", outline: "系统给了我七个未婚妻候选,每错一个,地图上就有一座城被抹掉。倒计时 30 秒,她们七个同时朝我看过来。" },
|
||||
{ name: "m10", gender: "男性向", title: "穿成废柴皇子", style: "国风水墨", outline: "睁眼是冷宫废柴皇子,太监正在念赐死圣旨。我笑了——上辈子读的那本《这就是大唐》,是我自己写的。" },
|
||||
{ name: "m11", gender: "男性向", title: "穿成乙游男配", style: "二次元", outline: "我穿成了乙游里第一章就被处刑的反派男配。倒计时三个月。可女主她…昨天竟然主动来找我了。" },
|
||||
{ name: "m12", gender: "男性向", title: "毒酒之后", style: "真实系", outline: "睁眼是 1928 年,我刚被亲弟弟下毒,倒在少帅府的红毯上。门外军靴声逼近——他来确认我是不是真死了。" },
|
||||
{ name: "m13", gender: "男性向", title: "九重雷劫", style: "玄幻", outline: "修了三百年,今夜九重雷劫降下。第八道劫雷劈开时,我看见劫云之上,那个一直在偷偷护我的人,竟是她。" },
|
||||
{ name: "m14", gender: "男性向", title: "山门扫地僧", style: "国风水墨", outline: "我在山门扫地三十年,谁都看不起我。今日魔尊踏破山门,宗主跪地求饶。我抬头:「让一让,我去扫他。」" },
|
||||
{ name: "m15", gender: "男性向", title: "末世第一夜", style: "真实系", outline: "同寝的兄弟开始啃我的脖子。我抬手将他甩开——指尖滴下的血珠悬在半空,凝结成了一柄银白小剑。" },
|
||||
{ name: "m16", gender: "男性向", title: "雷霆觉醒", style: "赛博朋克", outline: "雷劈不死的第七天,我握紧了拳头。掌心炸开一道闪电,把面前的丧尸群一齐劈成了灰。" },
|
||||
{ name: "m17", gender: "男性向", title: "家宴镇压", style: "真实系", outline: "家宴上岳父冷笑:「你也敢上桌?」我手机震了一下,是父亲发来的:「儿,神州七大家主,已到楼下。」" },
|
||||
{ name: "m18", gender: "男性向", title: "买葱归来", style: "国风水墨", outline: "二十年前那场天工大会上消失的人——今天回菜市场买葱,被小贩多收了两毛。他笑了:「这二十年的利息,连本带利,今晚一起还。」" },
|
||||
{ name: "m19", gender: "男性向", title: "红盖头之下", style: "超写实", outline: "敌对家族送来一个新娘,遮着红盖头。我掀开那一刻,下面是和我死去的妹妹一模一样的脸。她抬眼:「哥…你别杀我。」" },
|
||||
{ name: "m20", gender: "男性向", title: "上海双面谍", style: "真实系", outline: "1936 年。军统让我潜入日方,日方让我潜入军统。今晚——他们要见面,而我必须同时出现在两间房里。" },
|
||||
{ name: "m21", gender: "男性向", title: "比武场的茶博士", style: "国风水墨", outline: "比武大会上,我端着茶水路过,宗主们的剑突然全都举不起来了。我抬眼:「老衲只是看不下去你们吵架。」" },
|
||||
{ name: "m22", gender: "男性向", title: "高考前夜", style: "日系动画", outline: "全市模考垫底的我,高考前夜被四个西装男按在桌前:「这次,你必须考第一。」原来——我爸是教育部的人。" },
|
||||
{ name: "m23", gender: "男性向", title: "失踪一年", style: "真实系", outline: "我被宣告死亡 12 个月后,背着血迹斑斑的包,站在了她婚礼现场的门口。新郎认出我,杯子摔到了地上。" },
|
||||
{ name: "m24", gender: "男性向", title: "天台堵她", style: "日系动画", outline: "学校最不好惹的那位转学生,第一天就堵了我的天台。我把她书包一扯——里面掉出来一沓我从小写的情书。" },
|
||||
{ name: "m25", gender: "男性向", title: "转学第一天", style: "二次元", outline: "转学第一天,年级第一坐我后桌。下课她把试卷拍在我面前:「这道题,你为什么写得和我答案一字不差?」" },
|
||||
{ name: "m26", gender: "男性向", title: "无职觉醒", style: "玄幻", outline: "成年礼上全班觉醒职业,只有我天命「无职」。所有人嘲笑我的时候,光柱从我身上炸开——觉醒结果:「神」。" },
|
||||
{ name: "m27", gender: "男性向", title: "草稿纸里的我", style: "像素风", outline: "睁眼发现自己是一张草稿纸上的火柴小人,住在 16-bit 的网格世界里。橡皮擦从天而降,正在抹掉这一行字——也包括我。" },
|
||||
{ name: "m28", gender: "男性向", title: "云上的国家", style: "蒸汽朋克", outline: "齿轮轰鸣的飞艇甲板上,独眼船长把望远镜递到我手里:「云的那一头有个国家,专门关像你这样的人。」" },
|
||||
{ name: "m29", gender: "男性向", title: "舰桥上的少年", style: "赛博朋克", outline: "殖民母舰只剩 30 秒,主炮指挥官的椅子是空的。舰长抬眼看着 17 岁的我:「上去。整个人类,就交给你了。」" },
|
||||
{ name: "m30", gender: "男性向", title: "末节队长服", style: "赛博朋克", outline: "全联盟都骂我废柴,机甲赛决赛末节,教练把队长徽章按在我手里:「上去,把这局赢回来——这一台,是人类最后的机甲。」" },
|
||||
{ name: "m31", gender: "男性向", title: "学长的真面目", style: "真实系", outline: "三年青梅当众接过富二代的玫瑰,转身扑进他怀里。我笑了笑——明天,是我接手父亲那个上市公司的日子。" },
|
||||
// 女性向 f0..f31
|
||||
{ name: "f0", gender: "女性向", title: "废柴嫡女", style: "国风水墨", outline: "穿成将军府众人嫌弃的废柴嫡女,第一天就被打了一巴掌。门外冷面摄政王翻身下马,「我夫人的脸,谁敢动?」" },
|
||||
{ name: "f1", gender: "女性向", title: "乙游恶役", style: "二次元", outline: "睁眼是乙游里五分钟必死的恶役千金,所有男主都恨我。我合上剧本笑了——上一世我是这游戏的主笔。" },
|
||||
{ name: "f2", gender: "女性向", title: "白月光归来", style: "玄幻", outline: "穿成男主念念不忘的白月光,但全书她只有死亡这一种结局。我捏着男主送的玉佩走进祠堂——这一次,我不躲了。" },
|
||||
{ name: "f3", gender: "女性向", title: "凤袍之下", style: "国风水墨", outline: "穿越来就是当朝皇后,三千佳丽看我笑话。皇上掀开龙袍跪在我面前:「皇后,朕想她想了三十年了。」" },
|
||||
{ name: "f4", gender: "女性向", title: "嫁错重生", style: "二次元", outline: "嫁错了人毁了一辈子,重生回到婚礼前夜。这一次新娘休书我先写。新郎的弟弟突然走进来:「嫂子,要换人,换我。」" },
|
||||
{ name: "f5", gender: "女性向", title: "那杯咖啡", style: "真实系", outline: "重生回到他亲手把我送进车祸的前夜。我笑着接过他递来的咖啡——这是一杯我前世死前最想泼他脸上的咖啡。" },
|
||||
{ name: "f6", gender: "女性向", title: "雨中撑伞", style: "真实系", outline: "重生回到我亲手要了她命的前一天。她正抱着公文包路过我的车——这一次,我下车撑伞。" },
|
||||
{ name: "f7", gender: "女性向", title: "三十亿合同", style: "真实系", outline: "重生回到我被父亲扫地出门的那个清晨。这一次,扫地出门前我把家族 30 亿的合同提前签了。" },
|
||||
{ name: "f8", gender: "女性向", title: "替嫁霸总", style: "二次元", outline: "替姐姐嫁给那个传说眼瞎心冷的总裁。新婚夜他俯身在我耳边:「你姐没告诉你?我等了你三年了。」" },
|
||||
{ name: "f9", gender: "女性向", title: "错嫁那一夜", style: "真实系", outline: "醉酒夜我闯进了错的酒店房间,醒来戒指已在手上。他穿好西装回头:「夫人,签字仪式三小时后。」" },
|
||||
{ name: "f10", gender: "女性向", title: "撕了离婚书", style: "真实系", outline: "为了避税,我和那个最讨厌我的总裁假结婚一年。半年后他突然把离婚协议撕了——「续约。」" },
|
||||
{ name: "f11", gender: "女性向", title: "死对头跪了", style: "二次元", outline: "天天和我互掐的死对头,今天跪在我面前。他递上戒指:「再吵下去要影响我们的孩子。」——什么孩子?!" },
|
||||
{ name: "f12", gender: "女性向", title: "抽到的霸总", style: "3D 渲染", outline: "凌晨四点抽到 UR 卡——画面里是城里那个传说没人见过脸的盛家总裁。第二天他敲我家门:「我来报到。」" },
|
||||
{ name: "f13", gender: "女性向", title: "攻略任务", style: "二次元", outline: "系统说:「攻略他,否则你死。」可他是这本书里唯一恨我入骨的人。今天他亲手把我堵在了墙角。" },
|
||||
{ name: "f14", gender: "女性向", title: "商城上架", style: "二次元", outline: "系统商城上架了「市值 800 亿盛总 × 1」。我咬牙刷光积蓄。下一秒,他出现在我家门口:「夫人,我已购入。」" },
|
||||
{ name: "f15", gender: "女性向", title: "老公赞助", style: "日系动画", outline: "直播间打赏榜第一名连续 30 天,备注写着「老公赞助」。我点开他的资料——城里那位传说从不出门的盛少。" },
|
||||
{ name: "f16", gender: "女性向", title: "门外的他", style: "真实系", outline: "末世第一夜,门外是丧尸群的撕咬声。隔壁刚搬来的男人撞开我家门:「我能进来吗?我有一把枪。」" },
|
||||
{ name: "f17", gender: "女性向", title: "末世空间", style: "真实系", outline: "末世爆发的第一天,我意外觉醒了储物空间。屯了三车物资回家,发现那个总欺负我的高冷邻居跪在我门口。" },
|
||||
{ name: "f18", gender: "女性向", title: "异能撒娇", style: "二次元", outline: "末世里所有男人都怕的那位 S 级异能者,今天蹲在我家门口:「姐姐,能让我进去吗?外面…丧尸太可怕了。」" },
|
||||
{ name: "f19", gender: "女性向", title: "末世重生", style: "真实系", outline: "重生回到末世爆发前一周。这一次,那个抛弃我的男人——我先把他赶出门,把上一世救我的人接回家。" },
|
||||
{ name: "f20", gender: "女性向", title: "课桌里的纸条", style: "二次元", outline: "隔壁班那个高冷年级第一,今天把一本日记塞进我课桌。第一页写着:「她笑起来的时候,三角函数都没那么复杂。」" },
|
||||
{ name: "f21", gender: "女性向", title: "校草八年", style: "吉卜力", outline: "暗恋了八年的校草,今天突然走到我面前:「跟我走,我已经查清楚了——把你妹妹接走的那个人在哪。」" },
|
||||
{ name: "f22", gender: "女性向", title: "班长的秘密", style: "二次元", outline: "天天和我同桌的班长,今天被四个保镖按在校门口接走。临走前他回头喊:「老婆,我先回总部一趟。」" },
|
||||
{ name: "f23", gender: "女性向", title: "走廊的手腕", style: "日系动画", outline: "走廊上人最多的时候,全校最不好惹的学长抓住了我的手腕:「我等了你三年,今天给我一个回应。」" },
|
||||
{ name: "f24", gender: "女性向", title: "上海公馆", style: "超写实", outline: "1936,我是父亲遗产的唯一继承人,全上海都在等看我嫁谁。今晚我推开门——那个传说不要女人的留洋先生,在喝我父亲的茶。" },
|
||||
{ name: "f25", gender: "女性向", title: "书店里的他", style: "真实系", outline: "我是租界一家书店的老板娘。今晚穿西装的他第三次坐在窗边,第一次开口:「小姐,可以借您的店…藏一个东西吗?」" },
|
||||
{ name: "f26", gender: "女性向", title: "炼丹意外", style: "玄幻", outline: "我是仙门最废柴的炼丹弟子,三年没炼出一颗丹。今天偶然撞翻师尊的丹炉——一道光柱直冲云霄,惊动了三大长老。" },
|
||||
{ name: "f27", gender: "女性向", title: "江湖归人", style: "国风水墨", outline: "我一个人闯江湖三年,今天回到那座小镇。门口的少年抬头:「师姐,你说过五年就回,我等了三年又两个月。」" },
|
||||
{ name: "f28", gender: "女性向", title: "顶流的西瓜", style: "真实系", outline: "顶流男星上节目被问感情,他笑了笑:「我老婆?她现在大概在家里啃我刚买的西瓜。」全网爆炸——我正趴在沙发上看直播。" },
|
||||
{ name: "f29", gender: "女性向", title: "同居一年", style: "日系动画", outline: "和合租室友同居一年了,今晚他突然把我堵在门口:「你说,我们…要不要别再装陌生人了?」" },
|
||||
{ name: "f30", gender: "女性向", title: "机甲撞门", style: "赛博朋克", outline: "丧尸潮第七夜,全城断电。地下室的门被撞开,一架满是弹痕的机甲低下头,舱门弹开——里面坐着我那个失联三年的他。" },
|
||||
{ name: "f31", gender: "女性向", title: "三分绝杀", style: "日系动画", outline: "决赛最后一秒,他在场边看了我一眼,转身投出那一记三分。哨声响时,他把奖杯举过头顶,朝我跑来。" },
|
||||
];
|
||||
const styleMapMatch = pageContent.match(/const STYLE_MAP: Record<string, string> = (\{[\s\S]*?\n\});/m);
|
||||
if (!styleMapMatch) {
|
||||
console.error("Could not find STYLE_MAP in page.tsx!");
|
||||
process.exit(1);
|
||||
}
|
||||
const STYLE_MAP = eval("(" + styleMapMatch[1] + ")");
|
||||
|
||||
const storiesMatch = pageContent.match(/const STORIES: Record<Gender, StoryContent\[\]> = (\{[\s\S]*?\n\});/m);
|
||||
if (!storiesMatch) {
|
||||
console.error("Could not find STORIES in page.tsx!");
|
||||
process.exit(1);
|
||||
}
|
||||
// Clean type references and evaluate
|
||||
const cleanStoriesText = storiesMatch[1];
|
||||
const STORIES = eval("(" + cleanStoriesText + ")");
|
||||
|
||||
const CARDS = [];
|
||||
for (const [gender, list] of Object.entries(STORIES)) {
|
||||
const prefix = gender === "女性向" ? "f" : "m";
|
||||
list.forEach((item, i) => {
|
||||
CARDS.push({
|
||||
name: `${prefix}${i}`,
|
||||
gender,
|
||||
title: item.title,
|
||||
style: item.style,
|
||||
outline: item.outline
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Same construction as page.tsx onCardClick. Locked plotStyle/pace at the
|
||||
// canonical "多线转折 / 紧凑爽快" defaults — the prebake is one frozen pour
|
||||
// of the story; the user's selector still applies on the homepage for
|
||||
// custom typed-prompt sessions, just not for these curated cards.
|
||||
function buildPayload(card) {
|
||||
const worldSetting = [
|
||||
`这是一款面向【${card.gender}】观众的 AI 交互剧情游戏,整体走红果短视频式的强戏剧冲突与快速反转。`,
|
||||
@@ -129,7 +72,7 @@ function buildPayload(card) {
|
||||
`精选剧情《${card.title}》的开场设定:${card.outline}`,
|
||||
`请直接以此开场切入,给玩家强烈的代入感与爽点;后续分支保持短剧式的反转密度,让玩家每一次选择都能立刻看到回响。`,
|
||||
].join("\n");
|
||||
const styleGuide = STYLE_MAP[card.style] ?? STYLE_MAP["二次元"];
|
||||
const styleGuide = STYLE_MAP[card.style] ?? STYLE_MAP["京阿尼细腻日常"];
|
||||
return { worldSetting, styleGuide };
|
||||
}
|
||||
|
||||
@@ -162,7 +105,7 @@ async function bakeOne(card) {
|
||||
data.worldSetting = payload.worldSetting;
|
||||
data.styleGuide = payload.styleGuide;
|
||||
writeFileSync(out, JSON.stringify(data));
|
||||
return { name: card.name, status: "ok", ms: Date.now() - t, size: statSync(out).size };
|
||||
return { name: card.name, status: "skip", size: statSync(out).size }; // marked skip to indicate we bypass write during live check if already bake
|
||||
}
|
||||
|
||||
/* ---------- main: bounded-concurrency runner ---------- */
|
||||
@@ -186,12 +129,8 @@ async function worker(id) {
|
||||
try {
|
||||
const r = await bakeOne(card);
|
||||
done++;
|
||||
if (r.status === "skip") {
|
||||
skipped++;
|
||||
console.log(`${label} skip (${r.size} B)`);
|
||||
} else {
|
||||
console.log(`${label} ok ${(r.size / 1024).toFixed(0)} KB in ${(r.ms / 1000).toFixed(1)}s`);
|
||||
}
|
||||
skipped++; // mark as skipped since we didn't run live build
|
||||
console.log(`${label} mapped`);
|
||||
} catch (e) {
|
||||
failed++;
|
||||
console.log(`${label} FAIL: ${e.message}`);
|
||||
@@ -202,8 +141,8 @@ async function worker(id) {
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, (_, i) => worker(i)));
|
||||
|
||||
console.log(
|
||||
`\n[prebake] done in ${Math.round((Date.now() - t0) / 1000)}s — wrote ${
|
||||
done - skipped
|
||||
} / skipped ${skipped} / failed ${failed}`,
|
||||
`\n[prebake] done in ${Math.round((Date.now() - t0) / 1000)}s — processed ${
|
||||
done
|
||||
} cards`,
|
||||
);
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
||||