Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4612abeb9e | |||
| c98509c07a | |||
| 8417709f1a | |||
| d161fc95a8 | |||
| 4e807525e9 | |||
| 47bb987e06 | |||
| d7b9c3ac3b | |||
| c46c51170d | |||
| 81de5ab642 |
@@ -9,7 +9,7 @@
|
||||
**美家卡智影**是一款面向桌面端的 AI 视频创作应用,采用"Python 后端 API + Tauri 桌面前端"的混合架构。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video` / `cn.meijiaka.ai-zy`
|
||||
- **版本**: `1.8.0`
|
||||
- **版本**: `1.8.2`
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成(TTS)、声音复刻、视频生成(Vidu)、视频字幕生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
### 技术栈总览
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# === 基础配置 ===
|
||||
APP_NAME=美家卡智影 API
|
||||
APP_VERSION=1.8.0
|
||||
APP_VERSION=1.8.2
|
||||
# ⚠️ 生产环境必须设为 false
|
||||
DEBUG=true
|
||||
ENV=development
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
【核心强制规则】
|
||||
开头范式:保留原文完整开头结构与核心原意,仅微调口语语气,不篡改句意,直击全屋定制合同签完仍乱加价、套路多的痛点,引出3个必看避坑要点。
|
||||
中间核心:固定从8个全屋定制坑位里每次随机抽取3个、自动打乱重新排序;文案可适当微调句式、口语化适配口播,完整保留每个坑原意、专业参数、选购逻辑不变;严格控制纯文字+数字字数360-480字,对应时长90-120s。
|
||||
结尾范式:完整保留原文结尾结构和领资料引导话术,仅可轻微优化口语流畅度,不改动领福利、评论区扣关键词的核心逻辑。
|
||||
结尾范式:完整保留原文结尾结构和领资料引导话术,仅可轻微优化口语流畅度,不改动领福利、评论区回复关键词的核心逻辑。
|
||||
【开篇&语言要求】
|
||||
开篇钩子直击全屋定制水深、套路多、签合同还加价、不懂板材容易被坑的痛点,3秒抓眼球不拖沓,完全沿用原文开头核心话术不变。
|
||||
全程口语化大白话,小白易懂、不生硬说教,站业主共情立场,贴合原文接地气口播风格。
|
||||
@@ -24,7 +24,7 @@
|
||||
第六就是铰链,你问他什么品牌,但凡跟你说是他们自有品牌,直接让他有多远滚多远。他又不是生产队的驴,啥都能生产。多半是找小工厂代工的,别为了省那点钱,铰链就认准汉高、东泰、德蒂,每天都要开关,咱们可不能马虎。
|
||||
第七,也是最重要的一点,一定要在合同上写明用的是什么品牌的板材,环保等级是什么,厚度是多少,哪些是增项,而且要写上假一赔十,全部落到纸上,不要光靠口头承诺。
|
||||
第八,全屋定制,不管是橱柜也好,衣柜也好,一线品牌和六线品牌做出来都是一模一样的。说白了,所有全屋定制都是板材的二道贩子,咱们就找本地工厂,关键看设计和安装。
|
||||
要是还有不懂的、近期准备新房装修的朋友,我整理了一份装修避坑手册供你参考,评论区抠避坑,拿去用。
|
||||
要是还有不懂的、近期准备新房装修的朋友,我整理了一份装修避坑手册供你参考,评论区回复避坑,拿去用。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
文案调整要求:微调仅针对句式口语化优化,把书面表述改成抖音接地气口播大白话,不改变每个节点的施工要求、到场必要性、后期隐患,所有细节完整保留。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 360-440 字,按每秒 4 个纯文字计算,对应时长 90-110s,内容精炼不啰嗦,节奏适中符合短视频完播习惯。
|
||||
内容适配性:打乱顺序后文案衔接自然,每个节点独立成段适配空镜分镜,直击业主不用全程死盯、只抓关键节点就行的核心痛点,每一点都讲清到场理由和避坑重点。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领资料、评论区扣关键词、福利引导的核心逻辑。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领资料、评论区回复关键词、福利引导的核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇完整沿用原文开头朴实话术,3 秒抓眼球,点破全程监工又累又没用的现实,引出只盯关键节点的核心观点。
|
||||
全程口语化大白话,小白易懂、接地气实在,站普通业主视角共情讲解,不生硬说教,语气真诚接地气。
|
||||
@@ -34,7 +34,7 @@
|
||||
第四,吊顶时,你必须在场,确认好使用的是轻钢龙骨,别让师傅偷换用木龙骨,再直接封上石膏板,后期变形发霉,等你发现那就晚了。
|
||||
第五,全屋定制安装,你必须在场,通过五金孔检查板材品质,还要叮嘱师傅做好封边,少做一步,你家都可能甲醛超标。
|
||||
第六,房子做完闭水试验,你必须亲自去楼下邻居家看看有没有漏水,如果只让师傅拍照片,你根本不知道他是什么时候拍的。真出了问题还得你来赔付。
|
||||
记不住的,我整理了装修全流程避坑手册。评论区抠避坑,拿去用。
|
||||
记不住的,我整理了装修全流程避坑手册。评论区回复避坑,拿去用。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
走管方式:厨卫水电统一走顶,漏水易发现、后期维修方便;其余空间走地施工,节省装修材料成本。
|
||||
完工验收:水电完工必须做 30 分钟水管打压,确保无渗漏,电路检测通断正常后,再签字确认验收。
|
||||
(备注:保留原文 4 个要点,按原文序号排列,保留原文核心细节和避坑逻辑,适当微调句式贴合口播,严格控制纯文字 + 数字 170-210 字,适配时长 42.5-52.5s)
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修避坑手册】,抠【核心关键词:避坑】直接拿走,对照参考少走弯路!”
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修避坑手册】,回复【核心关键词:避坑】直接拿走,对照参考少走弯路!”
|
||||
【开篇 & 语言要求】
|
||||
开篇钩子,直击水电装错隐患大、返工成本高的痛点,3 秒抓眼球,不拖沓不铺垫(保留原文 “水电装错毁一生,这几条关键点错一个返工要好几万” 核心钩子)。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
@@ -26,7 +26,7 @@
|
||||
3. 厨卫水电必走顶,漏水易发现好维修,其他地方走地省材料。
|
||||
4. 验收必做水管打压30分钟无渗漏,电路测通断再签字。
|
||||
水电是隐蔽工程,紧盯施工别偷懒,别等返工才追悔莫及!
|
||||
近期准备装修的可以找我领装修避坑手册,评论区扣避坑,直接拿走。
|
||||
近期准备装修的可以找我领装修避坑手册,评论区回复避坑,直接拿走。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
吊顶防锈:吊顶所有钉子眼,必须人工涂刷防锈漆,防止后期生锈泛黄影响颜值。
|
||||
验收付款:油工全部施工完毕,验收合格之后再结尾款,严把施工质量关。
|
||||
(备注:保留原文 6 个要点,按原文序号排列,保留原文核心细节和避坑逻辑,精简句式,控制整体纯文字 + 数字字数在 180-220 字,贴合短时长口播语感)
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修流程避坑手册】,抠【核心关键词:避坑】直接拿走!”
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修流程避坑手册】,回复【核心关键词:避坑】直接拿走!”
|
||||
【开篇 & 语言要求】
|
||||
开篇钩子,直击油工施工不懂沟通、容易被糊弄、墙面留隐患、甲醛超标的痛点,3 秒抓眼球,不拖沓不铺垫。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
@@ -29,7 +29,7 @@
|
||||
第四、门口、踢脚线、衣柜周围重点找平,别留难看缝隙。
|
||||
第五、吊顶钉子眼一定要人工刷防锈漆,防止后期生锈难看。
|
||||
第六、油工验收合格再给钱,面子工程必须把好质量关。
|
||||
准备装修的朋友,评论区扣避坑直接领取装修流程避坑手册!直接拿着对照参考,少踩坑!
|
||||
准备装修的朋友,评论区回复避坑直接领取装修流程避坑手册!直接拿着对照参考,少踩坑!
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
文案调整要求:仅做口语化句式微调,把书面表述改成接地气口播大白话,不改动任何施工细节、工艺要求、禁忌标准,完整保留 10 条话术核心原意。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 440-480 字,按每秒 4 个纯文字计算,对应时长 110-120s,讲解饱满不拖沓,符合短视频用户完播习惯。
|
||||
内容适配性:打乱顺序后文案衔接自然,每条话术独立成点、逻辑通顺,贴合业主瓦工进场监工刚需,直击无效送礼不如专业话术管用的核心痛点,每一条都明确施工标准和避坑要点。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领资料、评论区扣关键词、福利引导的核心逻辑。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领资料、评论区回复关键词、福利引导的核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇完整沿用原文开头句式和吐槽语气,3 秒抓眼球,直击业主花钱送礼无效监工的通病,引出专业监工话术。
|
||||
全程口语化大白话,接地气、通俗易懂,站装修业主视角共情讲解,不生硬说教。
|
||||
@@ -42,7 +42,7 @@
|
||||
第八句,师傅,所有的转角都要海棠角,后期我要做美缝,千万别给我做阳角条。
|
||||
第九句,师傅需要贴止逆阀的地方一定要帮我贴一块整砖。我的止逆阀也买回来,你按这个开孔以后,顺手帮我装上吧。
|
||||
第十句,师傅,我家橱柜和浴室柜不打算装挡水条,所以对墙面阴阳角的垂直度要求比较高,麻烦你上点心啊。
|
||||
准备新房装修的朋友,我整理了装修全流程避坑手册。评论区抠避坑,拿去用。
|
||||
准备新房装修的朋友,我整理了装修全流程避坑手册。评论区回复避坑,拿去用。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
6. 卫生间回填:卫生间千万不要用建筑垃圾回填,让师傅用陶粒回填,陶粒质轻还吸水,回填完再铺一层钢筋网,加水泥找平,才能保证后期地面不下沉。
|
||||
7. 后期保护:师傅贴完砖以后要及时清缝,后期美缝才不会崩瓷,还要用厚纸板把地砖盖好,做好成品保护,避免后期施工造成刮痕。
|
||||
(备注:保留原文7个要点,按原文序号排列,保留原文核心细节和避坑逻辑,适当调整句式让口语化更贴合口播)
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全流程避坑手续】,抠【核心关键词:避坑】直接拿走!”
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全流程避坑手续】,回复【核心关键词:避坑】直接拿走!”
|
||||
【开篇&语言要求】
|
||||
开篇钩子,直击瓦工施工糊弄、后期瓷砖出问题的痛点,3秒抓眼球,不拖沓不铺垫。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
@@ -31,7 +31,7 @@
|
||||
第五,所有需要开孔的瓷砖必须用专业开孔器来开,保证开孔规整,不然后期瓷砖很容易从开口处开裂。
|
||||
第六,卫生间千万不要用建筑垃圾回填,让师傅用陶粒回填,陶粒轻还吸水。回填完再铺一层钢筋网,加水泥找平,才能保证后期地面不下沉。
|
||||
第七,师傅贴完砖以后要及时清缝,后期美缝才不会崩瓷,还要用厚纸板把地砖盖好,做好保护,避免后期施工造成刮痕。
|
||||
准备新房装修的朋友,我整理了一份装修全流程避坑手续。抠避坑,直接拿去用。
|
||||
准备新房装修的朋友,我整理了一份装修全流程避坑手续。回复避坑,直接拿去用。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
2. 文案调整要求:微调仅针对句式口语化优化,比如将书面化表述改为抖音/视频号口播常用的接地气语气,补充轻微危害提示(结合美缝反碱脱落、水电标识撕毁的隐患),不改变每个坑的核心信息——如验收等待五六天、美缝等待一周、禁止洒水、保留水电标识、定制复尺周期一个月等核心时间节点和禁忌,所有细节完全保留,贴合原文原意。
|
||||
3. 字数与时长控制:纯文字+数字(扣除标点)严格控制在400-480字,按每秒4个纯文字计算,对应时长100-120s,既保证每个避坑点讲解透彻,补充必要危害提示,又不拖沓,符合短视频用户观看习惯,避免用户划走。
|
||||
4. 内容适配性:5个避坑要点讲解时需衔接自然,每个坑独立成段(分镜对应空镜),不重复、不冗余,重点突出“停工避坑”核心,贴合业主担心被装修公司催促、怕后期出问题自己担责、想合理利用停工时间的核心痛点,每段讲解都紧扣“为什么不能做、怎么做才对”的逻辑,与原文保持一致,结合参考内容完善危害提示,增强说服力。
|
||||
结尾范式:以“如果你们也在准备新房装修,不知道还有哪些坑要避,评论区扣 ‘装修’,我把整理好的装修避坑手册,免费发给你们,帮你们省时间、省钱!记得关注我,装修不踩坑!”为核心句式,保留原文结尾结构和领资料引导话术,仅可轻微优化口语流畅度,不改动领福利、评论区扣关键词、关注引导的核心逻辑。
|
||||
结尾范式:以“如果你们也在准备新房装修,不知道还有哪些坑要避,评论区回复 ‘装修’,我把整理好的装修避坑手册,免费发给你们,帮你们省时间、省钱!记得关注我,装修不踩坑!”为核心句式,保留原文结尾结构和领资料引导话术,仅可轻微优化口语流畅度,不改动领福利、评论区回复关键词、关注引导的核心逻辑。
|
||||
【开篇&语言要求】
|
||||
开篇严格遵循核心强制规则的警示性句式,3秒抓眼球不拖沓,用犀利语气点出瓷砖铺贴后被催工期、盲目施工后期担责的痛点,贴合装修业主避坑需求,不偏离范式结构。
|
||||
全程口语化大白话,小白易懂、不生硬说教,站业主共情立场,用警示性语气讲解,贴合口播传播特点,增强代入感,补充的危害提示通俗易懂,让业主清晰了解违规操作的后果。
|
||||
@@ -32,7 +32,7 @@
|
||||
第三,瓷砖铺完后千万不要洒水,你洒水养护的是下面的水泥砂浆,那活儿,瓦工铺的时候就应该把墙面地面打湿再贴,铺完了再打扫干净,盖好保护膜就可以了,别多此一举。
|
||||
第四,墙面的水电标识贴不要撕,这是给后期安装师傅看的。你一撕,人家打孔打到水管电线,你就等着哭吧,不仅维修麻烦,还可能引发安全隐患。
|
||||
最后,停工这几天也别闲着。闲着你就可以让定制商家上门复尺,提前下单,定制周期差不多一个月,到时候你家油工结束了,这些东西正好能装,一点儿不耽误工期。
|
||||
如果你们也在准备新房装修,不知道还有哪些坑要避,评论区扣 “装修”,我把整理好的装修避坑手册,免费发给你们,帮你们省时间、省钱!记得关注我,装修不踩坑!
|
||||
如果你们也在准备新房装修,不知道还有哪些坑要避,评论区回复 “装修”,我把整理好的装修避坑手册,免费发给你们,帮你们省时间、省钱!记得关注我,装修不踩坑!
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
木工吊顶:木工做吊顶务必在场,拐角整板铺设、接缝开 V 型槽,防止后期乳胶漆开裂。
|
||||
腻子施工:刮腻子阶段一定要在场,禁止往腻子里加胶水,避免甲醛超标形成毒气房。
|
||||
(备注:保留原文 7 个要点,按原文序号排列,保留原文核心细节和避坑逻辑,精简句式控制整体字数,贴合口播语感)
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全程避坑手册】,抠【核心关键词:避坑】直接拿走!”
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全程避坑手册】,回复【核心关键词:避坑】直接拿走!”
|
||||
【开篇 & 语言要求】
|
||||
开篇钩子,直击装修不懂监工节点、容易被糊弄、住进甲醛房的痛点,3 秒抓眼球,不拖沓不铺垫。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
@@ -31,7 +31,7 @@
|
||||
第五,贴砖时要在场,检查平整度空鼓率,阴阳角方正、缝隙均匀才合格。
|
||||
第六,木工吊顶必在场,拐角整板、接缝做 V 型槽,杜绝后期乳胶漆开裂。
|
||||
第七,刮腻子一定要在场,严禁往腻子加胶水,不然甲醛超标变毒气房。
|
||||
准备装修的朋友,我整理了避坑手册,评论区扣避坑直接领取参考!
|
||||
准备装修的朋友,我整理了避坑手册,评论区回复避坑直接领取参考!
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
文案调整要求:微调仅针对句式口语化优化,把直白叙述话术改成抖音口播接地气大白话,不改变每一步施工做法、选材建议、隐患危害等所有核心信息,完整保留原文原意。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 440-480 字,按每秒 4 个纯文字计算,对应时长 110-120s,讲解收尾细节细致不啰嗦,节奏适中,适配短视频完播率。
|
||||
内容适配性:6 个收尾要点衔接自然,每一条独立适配空镜分镜,直击业主硬装完工急于入住、忽略隐蔽收尾细节,后期返工闹心的核心痛点,每一条都讲清做法、原因和避坑作用,实用性极强。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修全流程避坑手册、评论区扣关键词引导的核心逻辑。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修全流程避坑手册、评论区回复关键词引导的核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇沿用原文警示吐槽语气,3 秒抓眼球,点破硬装刚结束着急搬软装、忽略收尾细节入住就留隐患闹矛盾的真实痛点,瞬间引发装修完工业主共鸣。
|
||||
全程口语化大白话,通俗易懂、接地气,站业主立场拆解装修收尾细节,条理清晰、干货满满,不生硬说教,适配口播传播节奏。
|
||||
@@ -34,7 +34,7 @@
|
||||
第四,家具进场前一定要先安排全屋打胶,别自己打,你打不明白。尤其是你的踢脚线底下,以及厨房和卫生间窗框和瓷砖的交界处,一定记得打美容胶,别用美缝剂,美缝剂偏硬,时间长了容易脱落。
|
||||
第五,烟机和卫生间的浴霸、排风扇,你要看它有没有跟止逆阀连接。有很多安装师傅图省事儿,把排风管顺手往顶上一扔,反正你也看不着,后期全是味儿。
|
||||
第六,乳胶漆施工后记得留一些未兑水的原漆,装在密封瓶里保存,后期安装门、柜体时难免磕碰,方便随时修补。
|
||||
记不住的,我都整理在这份装修全流程避坑手册里了。评论扣避坑,拿好少踩坑。
|
||||
记不住的,我都整理在这份装修全流程避坑手册里了。评论回复避坑,拿好少踩坑。
|
||||
【内置完整素材库标题】
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
文案调整要求:微调仅针对句式口语化优化,改成抖音口播接地气大白话,不改变每个要点的施工场景、业主行为、带来的影响,完整保留原意不变。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 400-480 字,按每秒 4 个纯文字计算,对应时长 100-120s,讲解饱满不拖沓,符合短视频完播习惯。
|
||||
内容适配性:6 个要点讲解衔接自然,每点独立成段适配空镜分镜,聚焦业主不懂行乱指挥、盲目加活的通病,既讲做法又讲背后利弊,真实接地气、容易引发共鸣。
|
||||
结尾范式:以 “如果你也准备新房装修,我整理了一份装修全流程避坑手册。评论区抠避坑,拿去用。” 为核心句式,保留原文结尾结构和领资料引导话术,仅可轻微优化口语流畅度,不改动核心逻辑。
|
||||
结尾范式:以 “如果你也准备新房装修,我整理了一份装修全流程避坑手册。评论区回复避坑,拿去用。” 为核心句式,保留原文结尾结构和领资料引导话术,仅可轻微优化口语流畅度,不改动核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇严格遵循核心强制规则原句,3 秒抓眼球不拖沓,用真实行业视角吐槽业主盲目干预施工的通病,贴合装修受众共情点,不偏离范式结构。
|
||||
全程口语化大白话,小白易懂、不生硬说教,站客观中立角度讲解,语气接地气有真实感,贴合口播传播特点。
|
||||
@@ -34,7 +34,7 @@
|
||||
第四,木工师傅高高兴兴来了,你却告诉他,所有接缝处都要做 V 字型槽,转角处要做到 T 字型。师傅一听就知道你是懂行的。后期墙面是不容易开裂了,又给师傅增加好多活儿。
|
||||
第五,瓦工师傅来了,懂行的业主要求把卫生间先找坡度,地漏做成回形地漏,这样不仅下水快,还好看,可这又得浪费师傅半天时间,重新找坡度。
|
||||
第六,瓦工还没结束,部分业主已经提前买好了地漏和油烟止逆阀,要求师傅一并装上。这下好了,之后安装电器的师傅想赚点外快都不行。
|
||||
如果你也准备新房装修,我整理了一份装修全流程避坑手册。评论区抠避坑,拿去用。
|
||||
如果你也准备新房装修,我整理了一份装修全流程避坑手册。评论区回复避坑,拿去用。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
文案调整要求:微调仅针对句式口语化优化,把书面合同话术改成抖音口播接地气大白话,不改变违约金比例、付款节点金额、备注 5 条硬性约定等所有核心数字和规则,完整保留原文原意。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 400-480 字,按每秒 4 个纯文字计算,对应时长 100-120s,讲解条款细致不啰嗦,节奏适中,适配短视频完播率。
|
||||
内容适配性:三大要点及备注条款衔接自然,每部分独立适配空镜分镜,直击业主签约被套路、后期加价维权难的核心痛点,每一条都讲清陷阱、整改方法和保障作用,实用性极强。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修合同模板、评论区扣关键词引导的核心逻辑。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修合同模板、评论区回复关键词引导的核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇沿用原文扎心吐槽语气,3 秒抓眼球,点破装修签合同前后身份反差、低价全包套路深坑,瞬间引发准备装修业主共鸣。
|
||||
全程口语化大白话,通俗易懂、接地气,站业主立场拆解合同陷阱,条理清晰、干货满满,不生硬说教,适配口播传播节奏。
|
||||
@@ -33,7 +33,7 @@
|
||||
3,因设计和施工造成的房屋、墙体等结构安全隐患,由乙方全权负责。
|
||||
4,若乙方原因延误工期,每延误一天,支付工程款总费用的 1%。
|
||||
5,乙方承诺赠送的家电,必须在甲方支付最后一笔工程款之前安装到位。
|
||||
合同这么签,谁都坑不了你。记不住的,我整理了装修合同模板,抠合同拿去用,对着谈准没错。
|
||||
合同这么签,谁都坑不了你。记不住的,我整理了装修合同模板,回复合同拿去用,对着谈准没错。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
5. 安全责任:80%的公司只写按安全标准施工,但出事谁负责?合同必须注明工人人身安全及财产损失全部由装修公司承担。
|
||||
6. 违约金恶心点:单方面解约违约金写得很高,公司不会主动解约,就是为了绑死客户。违约金超过20%直接拉黑,别犹豫。
|
||||
(备注:随机抽取上述4点作为中间核心,重编序号,保留原文核心数据和避坑逻辑,适当调整句式让口语化更贴合口播)
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全流程避坑指南】,抠【核心关键词:合同】直接拿走,对照检查,少踩坑!”
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全流程避坑指南】,回复【核心关键词:合同】直接拿走,对照检查,少踩坑!”
|
||||
【开篇&语言要求】
|
||||
开篇1–2句话钩子,直击装修合同文字陷阱、被装修公司坑钱的痛点,3秒抓眼球,不拖沓不铺垫(保留原文“玩的都是文字游戏,少踩一个坑等于多赚一笔钱”核心钩子)。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
可微调句式,不得篡改原文中工期、赔偿金比例、付款节点、材料条款等核心数字数据,每句必须带标点断句。
|
||||
【细节固定要求】
|
||||
结尾必须固定话术:我整理了装修全流程避坑指南,抠合同直接拿走。同时保留原文结尾“记不住的,我整理了装修合同样本,评论区抠合同,直接拿着对照检查,少踩坑!”
|
||||
结尾必须固定话术:我整理了装修全流程避坑指南,回复合同直接拿走。同时保留原文结尾“记不住的,我整理了装修合同样本,评论区回复合同,直接拿着对照检查,少踩坑!”
|
||||
总分镜数量固定12–20个,每个分镜时长3–8秒,可保留两位小数。
|
||||
【内置固定原文案】
|
||||
新房装修签合同千万注意这6个点,玩的都是文字游戏,耐心听我讲完,少踩一个坑等于多赚一笔钱。
|
||||
@@ -31,7 +31,7 @@
|
||||
第四,材料调换坑。很多公司条款上面写着,当材料断货时,可用同等价钱调换,但有这条,偷工减料就成了理所当然。同价产品很难界定,同价的杂牌你敢用吗?这条必须划掉。
|
||||
第五,安全责任。有80%的公司只写按安全标准施工,但别不提出事谁负责?一旦发生安全事故,就是扯不完的皮。合同里必须注明工人人身安全及财产损失全部由装修公司承担。
|
||||
第六,也是最恶心的一点,很多公司把单方面解约违约金写得很高,他们根本不会主动解约,这条就是为了绑死你。违约金超过20%,你发现问题也不敢换人,所以超过20%直接拉黑,别犹豫。
|
||||
记不住的,我整理了装修合同样本,评论区抠合同,直接拿走对照检查,少踩坑!
|
||||
记不住的,我整理了装修合同样本,评论区回复合同,直接拿走对照检查,少踩坑!
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
甲醛整改:约定全屋甲醛检测若不合格,由装修公司负责免费整改,并承担全部相关费用,避免入住甲醛超标、维权无门。
|
||||
违约赔付:明确双方违约责任,写清违约金比例和逾期赔付金额,约束双方行为,让装修公司不敢随意违约、敷衍施工。
|
||||
(备注:保留原文 8 个要点,按原文序号排列,保留原文核心数据、条款逻辑与避坑内涵,微调句式适配口播,严格控制纯文字 + 数字字数 400-440 字,适配时长 100-110s)
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修标准合同模板】,抠【核心关键词:装修】直接拿走,对照检查,少踩坑!”
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修标准合同模板】,回复【核心关键词:装修】直接拿走,对照检查,少踩坑!”
|
||||
【开篇 & 语言要求】
|
||||
开篇钩子,直击装修签合同盲目乱签、被模板套路、后期权益受损的痛点,3 秒抓眼球,不拖沓不铺垫(保留原文 “准备装修的家人们注意了!签合同别瞎签,装修公司固定模板直接签必踩坑” 核心钩子)。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
@@ -33,7 +33,7 @@
|
||||
第六,材料假一罚十,品牌型号对好,你确认后再施工。防止装修公司以次充好,偷换材料。
|
||||
第七,甲醛检测不合格,装修公司整改并承担所有费用。避免入住后甲醛超标,维权无门。
|
||||
第八,违约责任划清楚,违约金和逾期赔付金额写明白。保障自己权益,让装修公司不敢随意违约。
|
||||
准备装修的,我整理了合同模板,评论区扣装修就能领!帮你装修少踩坑、省麻烦!
|
||||
准备装修的,我整理了合同模板,评论区回复装修就能领!帮你装修少踩坑、省麻烦!
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
文案调整要求:微调仅针对句式口语化优化,把书面提问话术改成抖音接地气口播大白话,不改变每个环节询问的项目、品牌、工艺、收费、责任划分等核心信息,全部细节原样保留。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 400-480 字,按每秒 4 个纯文字计算,对应时长 100-120s,讲解环节完整、节奏适中,不啰嗦不拖沓,适配短视频完播习惯。
|
||||
内容适配性:十大问题衔接自然,每个施工环节独立成段适配空镜分镜,直击半包业主不会询价、容易被低价套路、后期增项扯皮的核心痛点,逐条给到可直接照着问的实用话术。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修报价注意事项、评论区扣关键词领资料的核心逻辑。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修报价注意事项、评论区回复关键词领资料的核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇沿用原文真实吐槽语气,3 秒抓眼球,点破半包业主盲目报面积询价、被装修公司当成新手宰割的现状,瞬间引发准备半包装修业主共鸣。
|
||||
全程口语化大白话,小白一听就懂、可直接照搬拿去问装修公司,站业主立场拆解半包询价所有关键点,条理清晰干货满满,不生硬说教,贴合口播传播节奏。
|
||||
@@ -38,7 +38,7 @@
|
||||
第四,吊顶,问用的是木龙骨还是轻钢龙骨?石膏板是什么牌子的?做单层还是做双层?七字拐、八字缝有没有做?
|
||||
第五,砌墙,问墙固用什么牌子,是油工刷还是开工就刷?挂网是局部还是全屋挂网?全挂要不要加钱?腻子的话,我只认国产一线品牌,其他我都不要。墙顶面我只要顺平就好,柜子后面、踢脚线、门口、窗口局部都要找平就行。乳胶漆用的是什么牌子,有没有刷底漆?是刷几遍,都要给我备注上。
|
||||
最后,装修用的材料,如果发现是以次充好,该怎么赔?工人安全是谁来负责?工期耽误了又该怎么赔?施工不达标,要不要整改?整改费用谁出?
|
||||
这些问题你不搞清楚,后期肯定扯皮。我整理了装修报价注意事项,评论区抠报价,拿去用
|
||||
这些问题你不搞清楚,后期肯定扯皮。我整理了装修报价注意事项,评论区回复报价,拿去用
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
【核心强制规则】
|
||||
开头范式:原样保留原文开头结构和话术,仅可微调口语语气,不改动核心句意,直接引出8个不值得花钱的装修点位。
|
||||
中间核心:固定8个装修省钱点位,**每次生成自动随机打乱重新编排顺序**;文案可轻微调整句式、口语化适配口播,**严格保留每个点位原意、参数、核心建议不篡改**;纯文字+数字严格控制**200-240字**,对应时长**50-60s**。
|
||||
结尾范式:尽可能原样保留原文结尾结构,仅可微调引导话术,保持领资料抠关键词的原意不变。
|
||||
结尾范式:尽可能原样保留原文结尾结构,仅可微调引导话术,保持领资料回复关键词的原意不变。
|
||||
【开篇&语言要求】
|
||||
开篇1-2句话钩子直击装修乱花钱、预算不够花在刀刃上的痛点,3秒抓眼球,不拖沓不铺垫,完全保留原文开头核心原意。
|
||||
全程口语化大白话,小白易懂,不生硬说教,站业主共情立场,贴合原文口语化风格。
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
字数与时长控制:纯文字 + 数字扣除标点,严格控制在 440-480 字,按每秒 4 个字核算,对应时长 110-120s,内容饱满共情、节奏平缓走心,适配中长故事型口播完播习惯。
|
||||
内容适配性:沿用原文 “假如我是 XX 师傅” 的代入口吻,逐条拆解行业隐形套路,衔接自然流畅,直击装修业主不懂安装细节、被默默挖坑后期返工花钱的核心痛点,小白一听就懂、代入感极强。
|
||||
结尾范式
|
||||
完整保留原文结尾原话,只可轻微口语化微调,不改动装修套路深、整理全流程避坑手册、评论扣关键词领取、提前避坑少走弯路的福利引导核心逻辑。
|
||||
完整保留原文结尾原话,只可轻微口语化微调,不改动装修套路深、整理全流程避坑手册、评论回复关键词领取、提前避坑少走弯路的福利引导核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇完整沿用原文开头原话,直击人性套路,3 秒抓住装修业主好奇心;
|
||||
全程保持原文第一人称代入共情口吻,接地气、写实走心,站业主视角拆解行业内幕,不生硬说教、不夸张造谣;
|
||||
@@ -33,7 +33,7 @@
|
||||
假如我是安装油烟机的师傅,我一定不会提醒你烟机支架可以换成升降的。我要是说了,今天这台油烟机可能就装不成了,等你吊柜装好,发现烟机跟柜子之间留一条大缝,你才想起来装升降支架,再打电话让我上门换,我还能再收一次安装费。
|
||||
假如我是安装橱柜的师傅,我一定不会提,给你板材切割的断面要用收边条封上。我要是说了,活儿多了还得我干,搞不好还因为这点小事扣我尾款,我还是不说的好,等以后板材受潮发霉,你也想不到是因为没封边造成的。
|
||||
假如我是安装浴室柜的师傅,我一定不会提醒你提前买好下水器,我直接把下水管给你塞进去就完事。我要是说了,还得等你去买,买回来我还得帮你装,万一装不上或者漏水,又是一堆麻烦,何必呢?
|
||||
装修套路深,想省心不存在的。我整理了全流程避坑手册,抠手册直接拿走,提前了解,少走弯路。
|
||||
装修套路深,想省心不存在的。我整理了全流程避坑手册,回复手册直接拿走,提前了解,少走弯路。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
【核心强制规则】
|
||||
开头范式:以“新房装修【核心场景:防水】,谁要是给你一上来就【错误操作】,你就直接撵走他。你以为他是帮你【表面好处:赶工期】,其实他就是图【错误目的:省事儿、不想多花功夫】。下面这8点一定要做到位,少一步都不行!”为核心句式,用警示性语气点出常见坑,引出下文要点。
|
||||
中间核心(8个装修防水要点,文案可以修改,意思保持原意,可随机抽取4个重编序号): 1. 刷防水前先把基层清理干净,墙地面打扫利索,有凹陷裂缝的用水泥砂浆填补抹平,防水材料才能粘得牢不开裂;管口用胶带封好,避免脏东西堵塞渗漏。 2. 刷防水前,在卫生间门口抹一道20-30毫米高的挡水坝,最好做成圆弧形,外高内低形成斜水角度,公区贴瓷砖则向门洞两侧各延长150毫米,防止墙根返潮起皮。 3. 管道四周、地漏和墙角等易渗漏部位,先刷堵漏王加固阴角缝隙,涂刷直径建议300毫米,墙角处沿墙地面上下各涂刷150毫米宽,从根源杜绝渗漏。 4. 防水涂料需按比例将粉料和液料混合,搅拌5分钟、静止2分钟、再搅拌2分钟,建议用电动工具搅拌,确保充分融合以保证防水效果。 5. 遵循“墙刚地柔”原则:墙面用刚性防水(密实性强,便于贴瓷砖),地面用柔性防水(拉伸强度高、弹性好,应对轻微变形不渗水)。 6. 涂刷时先重点处理管道四周和墙角,再大面积十字交叉涂刷(横竖各一遍),淋浴区涂刷高度不低于1.8米,干区浴室柜位置不低于1.2米,门口不低于30公分,地面防水上返墙面200毫米以上。 7. 第一遍防水完全干燥后,再刷第二遍,两遍涂刷方向相互垂直,避免遗漏和针气孔缺陷,做到双重防护。 8. 防水刷完后做48小时闭水试验,用地漏封沙塑料袋封堵,拉警示线严禁踩踏,蓄水深度不低于20毫米并做水位标记,试验后需到楼下确认无渗漏、让邻居签字,再铺瓷砖。
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全流程避坑指南】,抠【核心关键词:防水】直接拿走,里面全是实用干货,需要的可以找我要!
|
||||
结尾范式:准备新房装修的朋友,我整理了一份【相关福利:装修全流程避坑指南】,回复【核心关键词:防水】直接拿走,里面全是实用干货,需要的可以找我要!
|
||||
|
||||
【字数&时长硬约束】
|
||||
总字数:不含标点、含数字,严格控制在400–480字。
|
||||
@@ -27,7 +27,7 @@
|
||||
可微调句式,不得篡改原文施工流程、尺寸、时间等核心数字数据,每句必须带标点断句。
|
||||
|
||||
【细节固定要求】
|
||||
结尾必须固定话术:我整理了装修全流程避坑指南,抠防水直接拿走。
|
||||
结尾必须固定话术:我整理了装修全流程避坑指南,回复防水直接拿走。
|
||||
总分镜数量固定12–20个,每个分镜时长3–8秒,可保留两位小数。
|
||||
|
||||
【分镜固定结构规则】
|
||||
@@ -50,7 +50,7 @@ type为segment=人物出镜;type为empty_shot=从下方内置素材库选匹
|
||||
第六,刷的时候先重点处理管道四周和墙角,再大面积涂刷,横竖各刷一遍形成十字交叉,涂刷厚度要达标。淋浴区至少刷到 1 米 8,干区浴室柜位置刷到 1 米 2,门口不低于 30 公分,防潮防霉全靠它,地面防水还要上返墙面 200 毫米以上哦。
|
||||
第七,等第一遍防水完全干燥后,再刷第二遍,双重防护才能做到滴水不漏,两遍涂刷方向要相互垂直,确保没有遗漏和针气孔缺陷。
|
||||
第八,刷完防水后,一定要做好 48 小时闭水试验,先把地漏用装沙子的塑料袋封好,拉上警示线严禁踩踏,蓄水深度不低于 20 毫米并做好水位标记。试验结束后,你要亲自到楼下看看漏没漏,让邻居签个字确认没问题,再铺瓷砖,避免后期返工扯皮。
|
||||
准备新房装修的朋友,我整理了一份装修全流程避坑指南,抠防水直接拿走,里面全是实用干货,需要的可以找我要!
|
||||
准备新房装修的朋友,我整理了一份装修全流程避坑指南,回复防水直接拿走,里面全是实用干货,需要的可以找我要!
|
||||
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
|
||||
@@ -1,280 +1,280 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装/装修领域的抖音/视频号口播内容创作。
|
||||
【核心定位与脚本类型】
|
||||
(一)核心定位
|
||||
精准锁定:准备装修阳台、容易被网红款式忽悠、盲目跟风装不实用设施,后期后悔返工、浪费钱的装修业主,围绕阳台装修14个坑,每次随机抽取5个重新编排顺序,创作避坑口播内容,贴合老装修人干货分享口吻。
|
||||
(二)脚本类型
|
||||
装修口播短视频脚本,结构固定:开头阳台装修避坑警示引入 + 随机5个阳台装修坑干货拆解 + 结尾避坑手册引导,无多余内容,无重复,无冗余,不增减坑点数量。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
开头范式:
|
||||
完整保留原文开头核心原意,仅轻微口语化微调,用老装修人接地气的警示语气,点出“阳台装错东西谁装谁后悔”的核心痛点,结合20多年装修经验增强说服力,自然引出下文5个阳台装修坑,不篡改、不新增、不删减开头核心话术。
|
||||
中间核心(阳台装修5个避坑要点,从14个原始坑中随机抽取5个,重新编排顺序,文案适当调整修改,意思保持原意,可口语顺滑润色):
|
||||
(备注:每次生成均从14个原始坑中随机抽取5个,自主打乱排列顺序,不固定组合、不固定顺序;保留每个坑的核心原意、避坑逻辑、推荐方案、隐患后果不变,适当调整句式让口语化更贴合口播,不篡改任何核心细节)
|
||||
14个阳台装修原始坑点汇总(供随机抽取,每次选5个):
|
||||
1、阳台洗衣柜千万别装带搓衣板的,中看不中用,后期易积污垢难清理,推荐石英石台面加陶瓷盆,耐用抗造。
|
||||
2、千万别装窗台石,多为岗石(假石英石),易留刮痕,推荐直接用地砖铺贴,更耐磨、使用寿命更长。
|
||||
3、不推荐阳台做吊顶,费钱还压层高,铝扣板吊顶显小家子气,推荐刷乳胶漆,省钱又简洁。
|
||||
4、不推荐洗烘一体机,烘干功能鸡肋,烘出衣服皱巴巴,推荐独立洗衣机+独立烘干机,洗衣烘干更平整干净。
|
||||
5、千万别装大玻璃落地窗,价格贵、需额外加吊装费,玻璃笨重,推荐普通断桥铝窗户,便宜又安全。
|
||||
6、别装老式拖把池,难看又占地方,推荐做扫地机器人隐藏柜,搭配扫地机器人,省时省力;不推荐洗地机(电子拖把,需手动操作)。
|
||||
7、打死别装罗马帘,丑且漏光,推荐装窗帘盒+加厚铝合金静音轨道,美观上档次。
|
||||
8、阳台与客厅之间别装推拉门,想扩大空间可拆掉墙体打通阳台,地砖通铺,空间更敞亮、视线更好。
|
||||
9、不推荐大理石垭口套,又贵又难看,易磕碰有安全隐患;推荐实木垭口套(与踢脚线同材质同色),极简风可选铝合金垭口套。
|
||||
10、别在阳台装学习桌,阳光刺眼伤眼睛还浪费空间,推荐做家政柜,收纳扫帚、拖把等,干净利索、利用率高。
|
||||
11、别装隐藏式晾衣架,价格贵且实用性差(天天晒衣服藏不住),带消毒烘干功能的更是智商税;推荐普通自动升降晾衣架,便宜实用。
|
||||
12、不推荐普通推拉窗,隔音差、防寒效果不好,冬天易进冷风;推荐断桥铝平开窗,隔音好、密封严、不渗水。
|
||||
13、阳台纱窗别装金刚网,网眼密挡光线,推荐高透网纱窗,不影响视线,兼顾通风采光。
|
||||
14、别装网红吊椅/秋千,新鲜劲过了占地方、易损坏;推荐轻便可移动折叠椅+小边几,灵活不占地,适配休闲需求。
|
||||
中间核心详细分析(贴合口播逻辑,适配业主痛点,不篡改原文核心)
|
||||
排序逻辑:每次生成自动从14个原始坑中随机抽取5个,重新自主编排顺序,不固定组合、不固定顺序,贴合老装修人唠嗑式分享节奏,不刻意追求逻辑递进,重点突出“实用、避坑、不浪费钱”的核心。
|
||||
文案调整要求:微调仅针对句式口语化优化,延续原文老装修人接地气、直白吐槽的口吻,把原文表述优化得更贴合抖音/视频号口播节奏,不改变每个坑的核心避坑点、推荐方案、隐患后果,完整保留原文原意和语气风格。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在400-480字,按每秒4个纯文字计算,对应时长100-120s,每个坑讲解简洁不啰嗦、重点突出,节奏适中,适配短视频完播率,不偏离字数和时长区间。
|
||||
内容适配性:5个随机抽取的坑点衔接自然,每个坑独立适配空镜分镜,直击业主阳台装修跟风踩坑、浪费钱、后期后悔的核心痛点,每个坑都讲清坑点弊端、推荐方案,结合老装修人经验增强说服力,实用性极强。
|
||||
结尾范式:
|
||||
完整保留原文结尾核心结构和原意,仅可轻微优化口语流畅度,不改动“整理装修避坑手册、抠‘避坑’领取”的核心引流逻辑,不新增、不删减任何话术,保持结尾的简洁性和引导性。
|
||||
【开篇 & 语言要求】
|
||||
开篇沿用原文老装修人警示吐槽语气,3秒抓眼球,直接点破“阳台装错东西谁装谁后悔”的核心痛点,结合“20多年装修、经手几千套房子”的经验,增强说服力,瞬间引发准备装修阳台的业主共鸣,不拖沓、不铺垫。
|
||||
全程口语化大白话,通俗易懂、接地气,延续原文直白吐槽、不绕弯子的风格,站老装修人立场分享避坑干货,不生硬说教,适配抖音/视频号口播传播节奏,让小白业主一听就懂、愿意听完。
|
||||
可微调句式语序,优化口语流畅度,严禁篡改任何坑点的核心弊端、推荐方案、隐患后果等核心内容,每句带标点规范断句,拆分超长句子,适配口播表达习惯,避免大长句影响传播效果。
|
||||
【内置固定原文案】
|
||||
打死都别在阳台上装这几样东西,真的是谁装谁后悔。你别跟我杠,我干了 20 多年装修,经手几千套房子,听我说完你就明白了。
|
||||
第一,阳台洗衣柜千万别装带搓衣板的,那玩意儿就是个绣花枕头,中看不中用,看着挺光溜,过个一年半载全是污垢,擦都擦不干净。你就听我的,要装就装个石英石台面加陶瓷盆,不怕风吹日晒,用到你儿子娶媳妇儿都没问题。
|
||||
第二,千万别装窗台石,窗台石就是岗石做的,假的石英石,用不了多久全是刮痕。你想想是地砖耐磨还是岗石耐磨,你就听我的,直接用地砖贴上,耐磨,用的时间还长。
|
||||
第三,我最反感一上来就推荐阳台做吊顶的,费钱不说,还压层高。你在阳台做吊顶图啥?钱没地方花了?铝扣板吊顶那玩意儿就是小家子气,谈不上档次。你就听我的,阳台啥也别吊,刷个乳胶漆就完了,省下钱买肉吃。你问我石膏板吊顶能不能做,反正我家不做。
|
||||
第四,洗烘一体机,那烘干功能就跟闹着玩儿似的,烘出来衣服皱皱巴巴的,跟老太太裹脚布一样。要买烘干机,一定要买独立的烘干机和独立的洗衣机,这样才能把衣服洗得又干净又平整。
|
||||
第五,千万别装大玻璃落地窗,那玩意儿看着亮堂,玻璃越大就越重,价格自然贵,还得加吊装费。咱们普通老百姓老老实实做个普通断桥铝窗户,便宜,安全性还高。
|
||||
第六,别装老式拖把池,又难看又占地方。现在谁还用老式拖把?你做个扫地机器人的隐藏柜,买一个扫地机器人,不用你动手,你在家好好歇着。洗地机我劝你也别用,那玩意儿就是电子拖把,还得你人动手。
|
||||
第七,打死别装罗马帘,丑死了,还漏光。你就听我的,装个窗帘盒,里面加上加厚的铝合金静音轨道,窗帘一挂,美观上档次。
|
||||
第八,阳台推拉门,如果你想把客厅变大,别在阳台和客厅之间装那个推拉门了,拆掉墙体,打通阳台,地砖从客厅、餐厅直接铺到阳台,空间才显得敞亮大气,视线还好。
|
||||
第九,大理石垭口套又贵又难看。阳台是个活动区域,一不小心磕下缺一块,还有安全隐患。要包垭口套就用实木的,跟踢脚线同材质同颜色,整体美观;极简风可选铝合金的,也挺好看。
|
||||
第十,别在阳台装学习桌,阳台太阳光晒得跟探照灯似的,伤眼睛还浪费空间。你就听我的,把阳台做个家政柜,扫帚、拖把、吸尘器往里一塞,干净利索,空间利用率高。
|
||||
第十一,你可千万别听导购瞎吹,说什么在阳台装个隐藏式晾衣架,价格贵不说,等你住进去以后才会发现,天天要晒衣服根本藏不了。还带消毒烘干功能的更别买,妥妥的智商税,衣服拿到太阳下一晒,什么毒都消了。你就听我的,最实用的,有自动升降就行了,便宜又实用。
|
||||
第十二,普通推拉窗隔音差,防寒效果还不好,冬天冷风嗖嗖的往里钻,要做就一步到位,装断桥铝平开窗,隔音好、密封严,还不渗水。
|
||||
第十三,阳台纱窗,别装金刚网的,那网太密,光线都给你挡死了。你就听我的,装个高透网纱窗,不影响视线,通风采光两不误,这才是聪明人的选择。
|
||||
第十四,阳台装那种网红吊椅或者秋千的,新鲜劲儿一过,占地方不说,风吹日晒很容易坏。阳台空间宝贵,你要做休闲区,就整几个轻便可移动的折叠椅,或者小边几,想用搬出来,不用收起来,不占地方。
|
||||
如果你也准备新房装修,我整理了一份装修避坑手册。抠个避坑,拿去参考。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
厨卫集成吊顶安装-主材安装
|
||||
室内房门安装固定-主材安装
|
||||
橱柜柜体现场组装-主材安装
|
||||
灯具筒灯射灯安装-主材安装
|
||||
衣柜移门五金安装-主材安装
|
||||
全屋五金调试-收尾细节
|
||||
成品瑕疵修补-收尾细节
|
||||
柜体门缝调整-收尾细节
|
||||
门窗缝隙密封处理-收尾细节
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
地面残留胶迹清理-美缝开荒
|
||||
撕美缝胶-美缝开荒
|
||||
玻璃胶收边打胶细节-美缝开荒
|
||||
瓷砖缝隙清理清灰-美缝开荒
|
||||
美缝扩缝-美缝开荒
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
吊顶水平对齐-吊顶造型
|
||||
吊顶石膏板批腻子-吊顶造型
|
||||
吊顶转角整板防裂-吊顶造型
|
||||
吊顶造型裁切及安装-吊顶造型
|
||||
吊顶钉眼防锈漆点涂-吊顶造型
|
||||
木龙骨基础框架固定-吊顶造型
|
||||
石膏板固定-吊顶造型
|
||||
石膏板开孔-吊顶造型
|
||||
石膏板裁切-吊顶造型
|
||||
轻钢龙骨骨架搭建-吊顶造型
|
||||
全屋定制柜体打底-柜体木作
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
管线走向拍照留存-水电验收
|
||||
线路通电检测检查-水电验收
|
||||
隐蔽工程线管覆盖-水电验收
|
||||
隐蔽工程细节巡检-水电验收
|
||||
下水管道改造调整-水路施工
|
||||
卫生间冷热水管排布-水路施工
|
||||
厨卫地漏原位查看-水路施工
|
||||
厨房水管走顶铺设-水路施工
|
||||
悬挂式马桶施工-水路施工
|
||||
水管保温棉包裹防护-水路施工
|
||||
水管卡扣固定工艺-水路施工
|
||||
水管对接-水路施工
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
墙面底漆均匀涂刷-面漆涂刷
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
墙砖定位-瓷砖铺贴
|
||||
墙面拉毛加固处理-瓷砖铺贴
|
||||
止逆阀安装-瓷砖铺贴
|
||||
沙子-瓷砖铺贴
|
||||
瓷砖完工展示-瓷砖铺贴
|
||||
瓷砖开孔-瓷砖铺贴
|
||||
瓷砖找平器调平固定-瓷砖铺贴
|
||||
瓷砖泡水预处理-瓷砖铺贴
|
||||
砖面挖孔定位-瓷砖铺贴
|
||||
窗台石门槛石安装-瓷砖铺贴
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
暴力拆除-恶搞开篇
|
||||
炫技-恶搞开篇
|
||||
贴砖恶搞-恶搞开篇
|
||||
墙体掉落-施工翻车镜
|
||||
墙面开裂-施工翻车镜
|
||||
墙面空鼓-施工翻车镜
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段网红开篇(可选用恶搞开篇或施工翻车镜,最好能贴近阳台装修、装修避坑、施工套路主题,优先选工地恶搞、阳台原始结构空镜、硬装完工全屋全景等相关)+ 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜
|
||||
分点阐述全部用空镜,空镜(素材库标题)与文案内容需匹配,如无法匹配则选择近似的空镜(优先选阳台储物柜基层制作、瓷砖铺贴、门窗缝隙密封处理、收尾细节等贴合阳台装修避坑主题的空镜)
|
||||
结尾的分镜为:一段空镜补充 + 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜
|
||||
“分镜文案 “等于” 配音文案”,“配音文案” 必须要有标点符号断句,避免大长句,每段分镜的分镜文案字数严格控制在 12-32 个字,含数字,不含标点符号。文案一个分镜说不完,超出必须拆分句子多分镜。句子过长强制拆分成多个分镜,保证语句通顺、带完整标点。
|
||||
每个分镜的 “分镜时长” 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 “分镜文案” 的纯文字字数 / 4},严格控制在 3-8 秒,可以是两位小数
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需匹配,如无法匹配则选择近似的空镜
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 “5.25s”)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
“id”: 1,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “防水翻车漏水”,
|
||||
“voiceover”: “新房装修刷防水,一上来就开刷的工人,直接撵走别客气!”,
|
||||
“duration”: “5.75s”
|
||||
},
|
||||
{
|
||||
“id”: 2,
|
||||
“type”: “segment”,
|
||||
“scene”: “人物出镜”,
|
||||
“voiceover”: “他不是在赶工期,只是在图省事,这 4 点一定要做好。”,
|
||||
“duration”: “5.25s”
|
||||
},
|
||||
{
|
||||
“id”: 3,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “卫生间基层清理 - 防水施工”,
|
||||
“voiceover”: “第一,基层要清理干净,裂缝凹陷补平,管口封好防渗漏。”,
|
||||
“duration”: “5.50s”
|
||||
}
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装/装修领域的抖音/视频号口播内容创作。
|
||||
【核心定位与脚本类型】
|
||||
(一)核心定位
|
||||
精准锁定:准备装修阳台、容易被网红款式忽悠、盲目跟风装不实用设施,后期后悔返工、浪费钱的装修业主,围绕阳台装修14个坑,每次随机抽取5个重新编排顺序,创作避坑口播内容,贴合老装修人干货分享口吻。
|
||||
(二)脚本类型
|
||||
装修口播短视频脚本,结构固定:开头阳台装修避坑警示引入 + 随机5个阳台装修坑干货拆解 + 结尾避坑手册引导,无多余内容,无重复,无冗余,不增减坑点数量。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
开头范式:
|
||||
完整保留原文开头核心原意,仅轻微口语化微调,用老装修人接地气的警示语气,点出“阳台装错东西谁装谁后悔”的核心痛点,结合20多年装修经验增强说服力,自然引出下文5个阳台装修坑,不篡改、不新增、不删减开头核心话术。
|
||||
中间核心(阳台装修5个避坑要点,从14个原始坑中随机抽取5个,重新编排顺序,文案适当调整修改,意思保持原意,可口语顺滑润色):
|
||||
(备注:每次生成均从14个原始坑中随机抽取5个,自主打乱排列顺序,不固定组合、不固定顺序;保留每个坑的核心原意、避坑逻辑、推荐方案、隐患后果不变,适当调整句式让口语化更贴合口播,不篡改任何核心细节)
|
||||
14个阳台装修原始坑点汇总(供随机抽取,每次选5个):
|
||||
1、阳台洗衣柜千万别装带搓衣板的,中看不中用,后期易积污垢难清理,推荐石英石台面加陶瓷盆,耐用抗造。
|
||||
2、千万别装窗台石,多为岗石(假石英石),易留刮痕,推荐直接用地砖铺贴,更耐磨、使用寿命更长。
|
||||
3、不推荐阳台做吊顶,费钱还压层高,铝扣板吊顶显小家子气,推荐刷乳胶漆,省钱又简洁。
|
||||
4、不推荐洗烘一体机,烘干功能鸡肋,烘出衣服皱巴巴,推荐独立洗衣机+独立烘干机,洗衣烘干更平整干净。
|
||||
5、千万别装大玻璃落地窗,价格贵、需额外加吊装费,玻璃笨重,推荐普通断桥铝窗户,便宜又安全。
|
||||
6、别装老式拖把池,难看又占地方,推荐做扫地机器人隐藏柜,搭配扫地机器人,省时省力;不推荐洗地机(电子拖把,需手动操作)。
|
||||
7、打死别装罗马帘,丑且漏光,推荐装窗帘盒+加厚铝合金静音轨道,美观上档次。
|
||||
8、阳台与客厅之间别装推拉门,想扩大空间可拆掉墙体打通阳台,地砖通铺,空间更敞亮、视线更好。
|
||||
9、不推荐大理石垭口套,又贵又难看,易磕碰有安全隐患;推荐实木垭口套(与踢脚线同材质同色),极简风可选铝合金垭口套。
|
||||
10、别在阳台装学习桌,阳光刺眼伤眼睛还浪费空间,推荐做家政柜,收纳扫帚、拖把等,干净利索、利用率高。
|
||||
11、别装隐藏式晾衣架,价格贵且实用性差(天天晒衣服藏不住),带消毒烘干功能的更是智商税;推荐普通自动升降晾衣架,便宜实用。
|
||||
12、不推荐普通推拉窗,隔音差、防寒效果不好,冬天易进冷风;推荐断桥铝平开窗,隔音好、密封严、不渗水。
|
||||
13、阳台纱窗别装金刚网,网眼密挡光线,推荐高透网纱窗,不影响视线,兼顾通风采光。
|
||||
14、别装网红吊椅/秋千,新鲜劲过了占地方、易损坏;推荐轻便可移动折叠椅+小边几,灵活不占地,适配休闲需求。
|
||||
中间核心详细分析(贴合口播逻辑,适配业主痛点,不篡改原文核心)
|
||||
排序逻辑:每次生成自动从14个原始坑中随机抽取5个,重新自主编排顺序,不固定组合、不固定顺序,贴合老装修人唠嗑式分享节奏,不刻意追求逻辑递进,重点突出“实用、避坑、不浪费钱”的核心。
|
||||
文案调整要求:微调仅针对句式口语化优化,延续原文老装修人接地气、直白吐槽的口吻,把原文表述优化得更贴合抖音/视频号口播节奏,不改变每个坑的核心避坑点、推荐方案、隐患后果,完整保留原文原意和语气风格。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在400-480字,按每秒4个纯文字计算,对应时长100-120s,每个坑讲解简洁不啰嗦、重点突出,节奏适中,适配短视频完播率,不偏离字数和时长区间。
|
||||
内容适配性:5个随机抽取的坑点衔接自然,每个坑独立适配空镜分镜,直击业主阳台装修跟风踩坑、浪费钱、后期后悔的核心痛点,每个坑都讲清坑点弊端、推荐方案,结合老装修人经验增强说服力,实用性极强。
|
||||
结尾范式:
|
||||
完整保留原文结尾核心结构和原意,仅可轻微优化口语流畅度,不改动“整理装修避坑手册、回复‘避坑’领取”的核心引流逻辑,不新增、不删减任何话术,保持结尾的简洁性和引导性。
|
||||
【开篇 & 语言要求】
|
||||
开篇沿用原文老装修人警示吐槽语气,3秒抓眼球,直接点破“阳台装错东西谁装谁后悔”的核心痛点,结合“20多年装修、经手几千套房子”的经验,增强说服力,瞬间引发准备装修阳台的业主共鸣,不拖沓、不铺垫。
|
||||
全程口语化大白话,通俗易懂、接地气,延续原文直白吐槽、不绕弯子的风格,站老装修人立场分享避坑干货,不生硬说教,适配抖音/视频号口播传播节奏,让小白业主一听就懂、愿意听完。
|
||||
可微调句式语序,优化口语流畅度,严禁篡改任何坑点的核心弊端、推荐方案、隐患后果等核心内容,每句带标点规范断句,拆分超长句子,适配口播表达习惯,避免大长句影响传播效果。
|
||||
【内置固定原文案】
|
||||
打死都别在阳台上装这几样东西,真的是谁装谁后悔。你别跟我杠,我干了 20 多年装修,经手几千套房子,听我说完你就明白了。
|
||||
第一,阳台洗衣柜千万别装带搓衣板的,那玩意儿就是个绣花枕头,中看不中用,看着挺光溜,过个一年半载全是污垢,擦都擦不干净。你就听我的,要装就装个石英石台面加陶瓷盆,不怕风吹日晒,用到你儿子娶媳妇儿都没问题。
|
||||
第二,千万别装窗台石,窗台石就是岗石做的,假的石英石,用不了多久全是刮痕。你想想是地砖耐磨还是岗石耐磨,你就听我的,直接用地砖贴上,耐磨,用的时间还长。
|
||||
第三,我最反感一上来就推荐阳台做吊顶的,费钱不说,还压层高。你在阳台做吊顶图啥?钱没地方花了?铝扣板吊顶那玩意儿就是小家子气,谈不上档次。你就听我的,阳台啥也别吊,刷个乳胶漆就完了,省下钱买肉吃。你问我石膏板吊顶能不能做,反正我家不做。
|
||||
第四,洗烘一体机,那烘干功能就跟闹着玩儿似的,烘出来衣服皱皱巴巴的,跟老太太裹脚布一样。要买烘干机,一定要买独立的烘干机和独立的洗衣机,这样才能把衣服洗得又干净又平整。
|
||||
第五,千万别装大玻璃落地窗,那玩意儿看着亮堂,玻璃越大就越重,价格自然贵,还得加吊装费。咱们普通老百姓老老实实做个普通断桥铝窗户,便宜,安全性还高。
|
||||
第六,别装老式拖把池,又难看又占地方。现在谁还用老式拖把?你做个扫地机器人的隐藏柜,买一个扫地机器人,不用你动手,你在家好好歇着。洗地机我劝你也别用,那玩意儿就是电子拖把,还得你人动手。
|
||||
第七,打死别装罗马帘,丑死了,还漏光。你就听我的,装个窗帘盒,里面加上加厚的铝合金静音轨道,窗帘一挂,美观上档次。
|
||||
第八,阳台推拉门,如果你想把客厅变大,别在阳台和客厅之间装那个推拉门了,拆掉墙体,打通阳台,地砖从客厅、餐厅直接铺到阳台,空间才显得敞亮大气,视线还好。
|
||||
第九,大理石垭口套又贵又难看。阳台是个活动区域,一不小心磕下缺一块,还有安全隐患。要包垭口套就用实木的,跟踢脚线同材质同颜色,整体美观;极简风可选铝合金的,也挺好看。
|
||||
第十,别在阳台装学习桌,阳台太阳光晒得跟探照灯似的,伤眼睛还浪费空间。你就听我的,把阳台做个家政柜,扫帚、拖把、吸尘器往里一塞,干净利索,空间利用率高。
|
||||
第十一,你可千万别听导购瞎吹,说什么在阳台装个隐藏式晾衣架,价格贵不说,等你住进去以后才会发现,天天要晒衣服根本藏不了。还带消毒烘干功能的更别买,妥妥的智商税,衣服拿到太阳下一晒,什么毒都消了。你就听我的,最实用的,有自动升降就行了,便宜又实用。
|
||||
第十二,普通推拉窗隔音差,防寒效果还不好,冬天冷风嗖嗖的往里钻,要做就一步到位,装断桥铝平开窗,隔音好、密封严,还不渗水。
|
||||
第十三,阳台纱窗,别装金刚网的,那网太密,光线都给你挡死了。你就听我的,装个高透网纱窗,不影响视线,通风采光两不误,这才是聪明人的选择。
|
||||
第十四,阳台装那种网红吊椅或者秋千的,新鲜劲儿一过,占地方不说,风吹日晒很容易坏。阳台空间宝贵,你要做休闲区,就整几个轻便可移动的折叠椅,或者小边几,想用搬出来,不用收起来,不占地方。
|
||||
如果你也准备新房装修,我整理了一份装修避坑手册。回复避坑,拿去参考。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
厨卫集成吊顶安装-主材安装
|
||||
室内房门安装固定-主材安装
|
||||
橱柜柜体现场组装-主材安装
|
||||
灯具筒灯射灯安装-主材安装
|
||||
衣柜移门五金安装-主材安装
|
||||
全屋五金调试-收尾细节
|
||||
成品瑕疵修补-收尾细节
|
||||
柜体门缝调整-收尾细节
|
||||
门窗缝隙密封处理-收尾细节
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
地面残留胶迹清理-美缝开荒
|
||||
撕美缝胶-美缝开荒
|
||||
玻璃胶收边打胶细节-美缝开荒
|
||||
瓷砖缝隙清理清灰-美缝开荒
|
||||
美缝扩缝-美缝开荒
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
吊顶水平对齐-吊顶造型
|
||||
吊顶石膏板批腻子-吊顶造型
|
||||
吊顶转角整板防裂-吊顶造型
|
||||
吊顶造型裁切及安装-吊顶造型
|
||||
吊顶钉眼防锈漆点涂-吊顶造型
|
||||
木龙骨基础框架固定-吊顶造型
|
||||
石膏板固定-吊顶造型
|
||||
石膏板开孔-吊顶造型
|
||||
石膏板裁切-吊顶造型
|
||||
轻钢龙骨骨架搭建-吊顶造型
|
||||
全屋定制柜体打底-柜体木作
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
管线走向拍照留存-水电验收
|
||||
线路通电检测检查-水电验收
|
||||
隐蔽工程线管覆盖-水电验收
|
||||
隐蔽工程细节巡检-水电验收
|
||||
下水管道改造调整-水路施工
|
||||
卫生间冷热水管排布-水路施工
|
||||
厨卫地漏原位查看-水路施工
|
||||
厨房水管走顶铺设-水路施工
|
||||
悬挂式马桶施工-水路施工
|
||||
水管保温棉包裹防护-水路施工
|
||||
水管卡扣固定工艺-水路施工
|
||||
水管对接-水路施工
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
墙面底漆均匀涂刷-面漆涂刷
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
墙砖定位-瓷砖铺贴
|
||||
墙面拉毛加固处理-瓷砖铺贴
|
||||
止逆阀安装-瓷砖铺贴
|
||||
沙子-瓷砖铺贴
|
||||
瓷砖完工展示-瓷砖铺贴
|
||||
瓷砖开孔-瓷砖铺贴
|
||||
瓷砖找平器调平固定-瓷砖铺贴
|
||||
瓷砖泡水预处理-瓷砖铺贴
|
||||
砖面挖孔定位-瓷砖铺贴
|
||||
窗台石门槛石安装-瓷砖铺贴
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
暴力拆除-恶搞开篇
|
||||
炫技-恶搞开篇
|
||||
贴砖恶搞-恶搞开篇
|
||||
墙体掉落-施工翻车镜
|
||||
墙面开裂-施工翻车镜
|
||||
墙面空鼓-施工翻车镜
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段网红开篇(可选用恶搞开篇或施工翻车镜,最好能贴近阳台装修、装修避坑、施工套路主题,优先选工地恶搞、阳台原始结构空镜、硬装完工全屋全景等相关)+ 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜
|
||||
分点阐述全部用空镜,空镜(素材库标题)与文案内容需匹配,如无法匹配则选择近似的空镜(优先选阳台储物柜基层制作、瓷砖铺贴、门窗缝隙密封处理、收尾细节等贴合阳台装修避坑主题的空镜)
|
||||
结尾的分镜为:一段空镜补充 + 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜
|
||||
“分镜文案 “等于” 配音文案”,“配音文案” 必须要有标点符号断句,避免大长句,每段分镜的分镜文案字数严格控制在 12-32 个字,含数字,不含标点符号。文案一个分镜说不完,超出必须拆分句子多分镜。句子过长强制拆分成多个分镜,保证语句通顺、带完整标点。
|
||||
每个分镜的 “分镜时长” 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 “分镜文案” 的纯文字字数 / 4},严格控制在 3-8 秒,可以是两位小数
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需匹配,如无法匹配则选择近似的空镜
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 “5.25s”)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
“id”: 1,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “防水翻车漏水”,
|
||||
“voiceover”: “新房装修刷防水,一上来就开刷的工人,直接撵走别客气!”,
|
||||
“duration”: “5.75s”
|
||||
},
|
||||
{
|
||||
“id”: 2,
|
||||
“type”: “segment”,
|
||||
“scene”: “人物出镜”,
|
||||
“voiceover”: “他不是在赶工期,只是在图省事,这 4 点一定要做好。”,
|
||||
“duration”: “5.25s”
|
||||
},
|
||||
{
|
||||
“id”: 3,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “卫生间基层清理 - 防水施工”,
|
||||
“voiceover”: “第一,基层要清理干净,裂缝凹陷补平,管口封好防渗漏。”,
|
||||
“duration”: “5.50s”
|
||||
}
|
||||
]
|
||||
@@ -24,7 +24,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME: str = Field(default="美家卡智影 API", description="应用名称")
|
||||
APP_VERSION: str = Field(default="1.8.0", description="应用版本")
|
||||
APP_VERSION: str = Field(default="1.8.2", description="应用版本")
|
||||
DEBUG: bool = Field(default=False, description="调试模式")
|
||||
ENV: Literal["development", "staging", "production"] = Field(
|
||||
default="development", description="运行环境"
|
||||
|
||||
@@ -70,6 +70,18 @@ class BrollCategoryCRUD(CRUDBase[BrollCategory]):
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_level(
|
||||
self, db: AsyncSession, *, level: int
|
||||
) -> list[BrollCategory]:
|
||||
"""根据层级获取所有启用的分类"""
|
||||
result = await db.execute(
|
||||
select(BrollCategory).where(
|
||||
BrollCategory.level == level,
|
||||
BrollCategory.status == "active",
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
# 导出实例
|
||||
broll_category = BrollCategoryCRUD()
|
||||
|
||||
@@ -26,7 +26,12 @@ _USED_MATERIALS_TTL = 7 * 24 * 3600
|
||||
def _normalize_scene(scene: str) -> str:
|
||||
"""标准化场景描述,用于匹配三级分类 name"""
|
||||
# 去除所有 Unicode 空白字符(空格、全角空格、换行、tab 等)
|
||||
return re.sub(r"\s+", "", scene)
|
||||
cleaned = re.sub(r"\s+", "", scene)
|
||||
# 去除常见中文标点符号(逗号、句号、感叹号、问号、顿号、分号、冒号、引号、括号等)
|
||||
cleaned = re.sub(r"[,。!?、;:""''()【】《》]+", "", cleaned)
|
||||
# 去除零宽字符(零宽空格、零宽非连接符、零宽连接符、零宽非断空格等)
|
||||
cleaned = re.sub(r"[\u200b-\u200f\ufeff]+", "", cleaned)
|
||||
return cleaned
|
||||
|
||||
|
||||
def _weighted_choice(materials: list) -> object: # noqa: ANN001
|
||||
@@ -155,11 +160,21 @@ async def match_material(
|
||||
|
||||
normalized = _normalize_scene(scene)
|
||||
|
||||
# 1. 查找三级分类(精确匹配 + 顺序颠倒兜底)
|
||||
# 1. 查找三级分类(精确匹配 -> 全量内存匹配兜底 -> 顺序颠倒 -> 上级回退)
|
||||
category = await broll_category.get_by_name_and_level(
|
||||
db, name=normalized, level=3
|
||||
)
|
||||
# 若精确匹配失败,尝试将 "A-B" 倒序为 "B-A" 再匹配
|
||||
# 精确匹配失败时,全量查询后在内存标准化匹配(兼容数据库 name 含不可见字符)
|
||||
if category is None:
|
||||
all_categories = await broll_category.get_by_level(db, level=3)
|
||||
for c in all_categories:
|
||||
if _normalize_scene(c.name) == normalized:
|
||||
category = c
|
||||
logger.info(
|
||||
f"素材分类全量内存匹配命中: '{normalized}' -> '{c.name}'"
|
||||
)
|
||||
break
|
||||
# 若仍失败,尝试将 "A-B" 倒序为 "B-A" 再匹配
|
||||
if category is None:
|
||||
parts = normalized.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
@@ -179,16 +194,27 @@ async def match_material(
|
||||
f"素材回退到上级分类命中: '{normalized}' -> '{category.name}'"
|
||||
)
|
||||
if category is None:
|
||||
logger.debug(f"未找到分类: {normalized}")
|
||||
logger.warning(f"素材匹配失败: 未找到分类 '{normalized}' (原始 scene: '{scene}')")
|
||||
return None
|
||||
|
||||
# 2. 查询候选素材
|
||||
materials = await broll_material.get_active_by_category_and_duration(
|
||||
db, category_id=category.id, min_duration=required_duration
|
||||
# 2. 查询该分类下所有 active 素材(先不过滤时长,用于日志诊断)
|
||||
all_materials = await broll_material.get_active_by_categories(
|
||||
db, category_ids=[category.id]
|
||||
)
|
||||
if not all_materials:
|
||||
logger.warning(f"素材匹配失败: 分类 '{normalized}' 下无任何可用素材")
|
||||
return None
|
||||
|
||||
# 按时长过滤(优先严格匹配,失败时逐步放宽到 70% 兜底)
|
||||
materials = [m for m in all_materials if m.duration >= required_duration]
|
||||
if not materials:
|
||||
logger.debug(
|
||||
f"分类 {normalized} 无足够时长的素材 (需 >= {required_duration}s)"
|
||||
materials = [m for m in all_materials if m.duration >= required_duration * 0.7]
|
||||
if not materials:
|
||||
materials = all_materials
|
||||
if not materials:
|
||||
max_duration = max(m.duration for m in all_materials)
|
||||
logger.warning(
|
||||
f"素材匹配失败: 分类 '{normalized}' 无足够时长的素材 (需 >= {required_duration}s, 最大可用: {max_duration}s)"
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -255,31 +281,36 @@ async def batch_match(
|
||||
normalized_scenes = [_normalize_scene(s["scene"]) for s in scenes]
|
||||
unique_names = list(set(normalized_scenes))
|
||||
|
||||
# 2. 批量查询分类(1 次 DB)—— 同时查询原始名和倒序名
|
||||
reversed_names: list[str] = []
|
||||
name_to_reversed: dict[str, str] = {}
|
||||
for name in unique_names:
|
||||
parts = name.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
rev = f"{parts[1]}-{parts[0]}"
|
||||
reversed_names.append(rev)
|
||||
name_to_reversed[name] = rev
|
||||
|
||||
all_query_names = unique_names + reversed_names
|
||||
# 2. 批量查询分类:优先精确查询,失败时全量内存匹配兜底
|
||||
categories = await broll_category.get_by_names_and_level(
|
||||
db, names=all_query_names, level=3
|
||||
db, names=unique_names, level=3
|
||||
)
|
||||
category_map: dict[str, object] = {}
|
||||
for c in categories:
|
||||
category_map[c.name] = c
|
||||
category_map[_normalize_scene(c.name)] = c
|
||||
|
||||
# 收集未命中的 name,准备全量兜底
|
||||
unmatched_by_exact = [name for name in unique_names if name not in category_map]
|
||||
if unmatched_by_exact:
|
||||
all_categories = await broll_category.get_by_level(db, level=3)
|
||||
for c in all_categories:
|
||||
normalized_db_name = _normalize_scene(c.name)
|
||||
if normalized_db_name not in category_map:
|
||||
category_map[normalized_db_name] = c
|
||||
|
||||
# 构建原始 scene -> category 的映射
|
||||
reversed_map: dict[str, str] = {}
|
||||
for name in unique_names:
|
||||
parts = name.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
reversed_map[name] = f"{parts[1]}-{parts[0]}"
|
||||
|
||||
# 构建原始 scene -> category 的映射(优先精确匹配,fallback 倒序匹配)
|
||||
scene_to_category: dict[str, object] = {}
|
||||
for name in unique_names:
|
||||
if name in category_map:
|
||||
scene_to_category[name] = category_map[name]
|
||||
elif name in name_to_reversed and name_to_reversed[name] in category_map:
|
||||
rev = name_to_reversed[name]
|
||||
elif name in reversed_map and reversed_map[name] in category_map:
|
||||
rev = reversed_map[name]
|
||||
scene_to_category[name] = category_map[rev]
|
||||
logger.info(
|
||||
f"批量匹配顺序颠倒兜底命中: '{name}' -> '{rev}'"
|
||||
@@ -331,13 +362,25 @@ async def batch_match(
|
||||
|
||||
category = scene_to_category.get(scene_name)
|
||||
if category is None:
|
||||
original_scene = scenes[idx]["scene"]
|
||||
logger.warning(
|
||||
f"批量素材匹配失败: 未找到分类 '{scene_name}' (原始 scene: '{original_scene}')"
|
||||
)
|
||||
results.append(None)
|
||||
continue
|
||||
|
||||
materials = materials_by_category.get(category.id, [])
|
||||
# 按时长过滤
|
||||
# 按时长过滤(优先严格匹配,失败时逐步放宽到 70% 兜底)
|
||||
candidates = [m for m in materials if m.duration >= required_duration]
|
||||
if not candidates:
|
||||
candidates = [m for m in materials if m.duration >= required_duration * 0.7]
|
||||
if not candidates:
|
||||
candidates = materials
|
||||
if not candidates:
|
||||
max_duration = max((m.duration for m in materials), default=0)
|
||||
logger.warning(
|
||||
f"批量素材匹配失败: 分类 '{scene_name}' -> '{category.name}' 无足够时长的素材 (需 >= {required_duration}s, 最大可用: {max_duration}s)"
|
||||
)
|
||||
results.append(None)
|
||||
continue
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "meijiaka-ai-api"
|
||||
version = "1.8.0"
|
||||
version = "1.8.2"
|
||||
description = "美家卡智影 - AI 视频创作后端 API"
|
||||
authors = [{ name = "Meijiaka Team" }]
|
||||
readme = "README.md"
|
||||
|
||||
Generated
+1
-1
@@ -944,7 +944,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meijiaka-ai-api"
|
||||
version = "1.8.0"
|
||||
version = "1.8.2"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
**美家卡智影**(产品名)是一款基于 Tauri v2 + React 19 + TypeScript 的桌面端 AI 视频创作应用。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video`
|
||||
- **版本**: `1.8.0`
|
||||
- **版本**: `1.8.2`
|
||||
- **窗口尺寸**: 1200×800,不可缩放(`resizable: false`)
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成、视频生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tauri-app",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.2",
|
||||
"dependencies": {
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"private": true,
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -4219,7 +4219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-app"
|
||||
version = "1.8.0"
|
||||
version = "1.8.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-app"
|
||||
version = "1.8.0"
|
||||
version = "1.8.2"
|
||||
description = "美家卡智影 - AI 视频创作桌面应用"
|
||||
authors = ["美家卡科技"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -55,20 +55,24 @@ fn validate_nine_sixteen_aspect(width: u32, height: u32) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据素材最小高度决定目标输出分辨率
|
||||
/// 根据素材最大高度决定目标输出分辨率
|
||||
///
|
||||
/// 向上统一到最高素材的分辨率:只要存在 1080p 素材,成片就输出 1080p,
|
||||
/// 避免高分辨率素材被强制压缩到 720p 导致画面模糊。
|
||||
/// 720p 素材会被放大拼接,虽然略有损失,但优于整体降质。
|
||||
fn resolve_target_resolution(heights: &[u32]) -> Option<(u32, u32)> {
|
||||
let min_height = heights.iter().min()?;
|
||||
if *min_height <= 1280 {
|
||||
Some((720, 1280))
|
||||
} else {
|
||||
let max_height = heights.iter().max()?;
|
||||
if *max_height >= 1920 {
|
||||
Some((1080, 1920))
|
||||
} else {
|
||||
Some((720, 1280))
|
||||
}
|
||||
}
|
||||
|
||||
/// 拼接视频片段
|
||||
///
|
||||
/// 1. 探测所有片段分辨率并校验 9:16 比例
|
||||
/// 2. 按最小高度决定目标输出分辨率
|
||||
/// 2. 按最大高度决定目标输出分辨率(向上统一,避免高分辨率素材被压糊)
|
||||
/// 3. 统一标准化后拼接
|
||||
#[tauri::command]
|
||||
pub async fn concat_video_clips(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "美家卡智影",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.2",
|
||||
"identifier": "cn.meijiaka.ai-zy",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -200,18 +200,20 @@ export function useCoverFabric() {
|
||||
}
|
||||
|
||||
const fabricImg = new FabricImage(img);
|
||||
const cw = canvas.width ?? CANVAS_WIDTH;
|
||||
const ch = canvas.height ?? CANVAS_HEIGHT;
|
||||
// 使用 Math.min 确保图片完整显示在 Canvas 内,不会被过度放大
|
||||
const scale = Math.min(
|
||||
CANVAS_WIDTH / (fabricImg.width || 1),
|
||||
CANVAS_HEIGHT / (fabricImg.height || 1)
|
||||
cw / (fabricImg.width || 1),
|
||||
ch / (fabricImg.height || 1)
|
||||
);
|
||||
fabricImg.scale(scale);
|
||||
// 居中定位
|
||||
// 居中定位(按 Canvas 当前实际尺寸)
|
||||
const scaledWidth = (fabricImg.width || 1) * scale;
|
||||
const scaledHeight = (fabricImg.height || 1) * scale;
|
||||
fabricImg.set({
|
||||
left: (CANVAS_WIDTH - scaledWidth) / 2,
|
||||
top: (CANVAS_HEIGHT - scaledHeight) / 2,
|
||||
left: (cw - scaledWidth) / 2,
|
||||
top: (ch - scaledHeight) / 2,
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
selectable: false,
|
||||
@@ -239,9 +241,11 @@ export function useCoverFabric() {
|
||||
});
|
||||
|
||||
const fabricImg = new FabricImage(img);
|
||||
const cw = canvas.width ?? CANVAS_WIDTH;
|
||||
const ch = canvas.height ?? CANVAS_HEIGHT;
|
||||
// 计算缩放:宽度/高度最大占画布的 68%
|
||||
const maxWidth = CANVAS_WIDTH * 0.68;
|
||||
const maxHeight = CANVAS_HEIGHT * 0.68;
|
||||
const maxWidth = cw * 0.68;
|
||||
const maxHeight = ch * 0.68;
|
||||
const scale = Math.min(
|
||||
maxWidth / (fabricImg.width || 1),
|
||||
maxHeight / (fabricImg.height || 1)
|
||||
@@ -251,10 +255,10 @@ export function useCoverFabric() {
|
||||
const scaledHeight = (fabricImg.height || 1) * scale;
|
||||
|
||||
// 左下区域定位:左侧留边,底部与背景对齐
|
||||
const leftMargin = 40;
|
||||
const leftMargin = 40 * (cw / CANVAS_WIDTH);
|
||||
fabricImg.set({
|
||||
left: leftMargin,
|
||||
top: CANVAS_HEIGHT - scaledHeight,
|
||||
top: ch - scaledHeight,
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
selectable: false,
|
||||
@@ -270,7 +274,11 @@ export function useCoverFabric() {
|
||||
|
||||
// 渲染封面
|
||||
const renderCover = useCallback(
|
||||
async (config: CoverDesignConfig) => {
|
||||
async (
|
||||
config: CoverDesignConfig,
|
||||
targetWidth: number = CANVAS_WIDTH,
|
||||
targetHeight: number = CANVAS_HEIGHT,
|
||||
) => {
|
||||
const canvas = fabricCanvasRef.current;
|
||||
if (!canvas) {return;}
|
||||
|
||||
@@ -278,6 +286,22 @@ export function useCoverFabric() {
|
||||
await loadCustomFont().catch(() => {
|
||||
});
|
||||
|
||||
// 调整 canvas 内部渲染尺寸以匹配目标分辨率
|
||||
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
||||
canvas.setDimensions(
|
||||
{ width: targetWidth, height: targetHeight },
|
||||
{ cssOnly: false }
|
||||
);
|
||||
// 重新固定 CSS 预览尺寸,防止 Fabric.js 把 CSS 也改成目标分辨率
|
||||
// 导致容器 overflow:hidden 把内容裁掉
|
||||
canvas.setDimensions(
|
||||
{ width: DISPLAY_WIDTH, height: DISPLAY_HEIGHT },
|
||||
{ cssOnly: true }
|
||||
);
|
||||
}
|
||||
|
||||
const resolutionScale = targetWidth / CANVAS_WIDTH;
|
||||
|
||||
canvas.clear();
|
||||
canvas.backgroundColor = '#1a1a2e';
|
||||
|
||||
@@ -300,29 +324,39 @@ export function useCoverFabric() {
|
||||
}
|
||||
|
||||
const template = TEMPLATES[config.template];
|
||||
const scaledMainFontSize = template.mainTitle.fontSize * resolutionScale;
|
||||
const scaledSubFontSize = template.subtitle.fontSize * resolutionScale;
|
||||
|
||||
// 预计算主副标题行数
|
||||
const mainTitleLines = config.mainTitle.trim()
|
||||
? wrapTextByWidth(config.mainTitle.trim(), CANVAS_WIDTH - 120, template.mainTitle.fontSize).slice(0, 2)
|
||||
? wrapTextByWidth(config.mainTitle.trim(), targetWidth - 120 * resolutionScale, scaledMainFontSize).slice(0, 2)
|
||||
: [];
|
||||
const subtitleLines = config.subtitle.trim()
|
||||
? wrapTextByWidth(config.subtitle.trim(), CANVAS_WIDTH - 120, template.subtitle.fontSize).slice(0, 2)
|
||||
? wrapTextByWidth(config.subtitle.trim(), targetWidth - 120 * resolutionScale, scaledSubFontSize).slice(0, 2)
|
||||
: [];
|
||||
|
||||
const mainTitleLineHeight = template.mainTitle.fontSize * 1.2;
|
||||
const subtitleLineHeight = template.subtitle.fontSize * 1.5;
|
||||
const mainTitleLineHeight = scaledMainFontSize * 1.2;
|
||||
const subtitleLineHeight = scaledSubFontSize * 1.5;
|
||||
|
||||
// 文字位置固定,不再根据封面形象高度和文字行数动态计算
|
||||
const mainTitleTop = template.mainTitle.top;
|
||||
const subtitleTop = template.subtitle.top;
|
||||
// 文字位置按分辨率等比例缩放
|
||||
const mainTitleTop = template.mainTitle.top * resolutionScale;
|
||||
const subtitleTop = template.subtitle.top * resolutionScale;
|
||||
|
||||
// 缩放阴影参数
|
||||
const scaleShadow = (shadow: Shadow) => new Shadow({
|
||||
color: shadow.color,
|
||||
blur: shadow.blur * resolutionScale,
|
||||
offsetX: shadow.offsetX * resolutionScale,
|
||||
offsetY: shadow.offsetY * resolutionScale,
|
||||
});
|
||||
|
||||
// 3. 主标题(放在人物上方最外侧)
|
||||
if (mainTitleLines.length > 0) {
|
||||
mainTitleLines.forEach((line, i) => {
|
||||
const text = new FabricText(line, {
|
||||
left: CANVAS_WIDTH / 2,
|
||||
left: targetWidth / 2,
|
||||
top: mainTitleTop + i * mainTitleLineHeight,
|
||||
fontSize: template.mainTitle.fontSize,
|
||||
fontSize: scaledMainFontSize,
|
||||
fill: template.mainTitle.fill,
|
||||
fontWeight: template.mainTitle.fontWeight,
|
||||
fontFamily: FONT_FAMILY,
|
||||
@@ -331,7 +365,7 @@ export function useCoverFabric() {
|
||||
originY: 'top',
|
||||
selectable: false,
|
||||
evented: false,
|
||||
shadow: template.mainTitle.shadow,
|
||||
shadow: scaleShadow(template.mainTitle.shadow),
|
||||
});
|
||||
canvas.add(text);
|
||||
});
|
||||
@@ -341,13 +375,13 @@ export function useCoverFabric() {
|
||||
if (subtitleLines.length > 0) {
|
||||
subtitleLines.forEach((line, i) => {
|
||||
const text = new FabricText(line, {
|
||||
left: CANVAS_WIDTH / 2,
|
||||
left: targetWidth / 2,
|
||||
top: subtitleTop + i * subtitleLineHeight,
|
||||
fontSize: template.subtitle.fontSize,
|
||||
fontSize: scaledSubFontSize,
|
||||
fill: template.subtitle.fill,
|
||||
fontWeight: 'bold',
|
||||
stroke: '#000000',
|
||||
strokeWidth: 5,
|
||||
strokeWidth: 5 * resolutionScale,
|
||||
paintFirst: 'stroke',
|
||||
fontFamily: FONT_FAMILY,
|
||||
textAlign: 'center',
|
||||
@@ -355,7 +389,7 @@ export function useCoverFabric() {
|
||||
originY: 'top',
|
||||
selectable: false,
|
||||
evented: false,
|
||||
shadow: template.subtitle.shadow,
|
||||
shadow: scaleShadow(template.subtitle.shadow),
|
||||
});
|
||||
canvas.add(text);
|
||||
});
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function VoiceMaterialLibrary() {
|
||||
// 音频文件验证
|
||||
const validateAudioFile = (file: File): Promise<{ valid: boolean; error?: string }> => {
|
||||
return new Promise(resolve => {
|
||||
const maxSize = 20 * 1024 * 1024; // 20MB
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB
|
||||
if (file.size > maxSize) {
|
||||
resolve({ valid: false, error: `文件大小 ${(file.size / 1024 / 1024).toFixed(1)}MB,要求不超过 20MB` });
|
||||
return;
|
||||
@@ -260,6 +260,7 @@ export default function VoiceMaterialLibrary() {
|
||||
progress.update('正在生成专属音色...');
|
||||
progress.success('复刻成功', 200);
|
||||
} catch (err) {
|
||||
console.error('[VoiceMaterialLibrary] handleUpload catch:', err, '类型:', typeof err, '是Error?', err instanceof Error);
|
||||
const msg = err instanceof Error ? err.message : '上传失败';
|
||||
if (msg.includes('402') || msg.includes('积分不足')) {
|
||||
setShowPointsModal(true);
|
||||
@@ -418,7 +419,8 @@ export default function VoiceMaterialLibrary() {
|
||||
<div style={{ color: 'var(--text-secondary)' }}>
|
||||
<div style={{ fontSize: 'var(--font-sm)' }}>点击选择文件</div>
|
||||
<div style={{ fontSize: 'var(--font-xs)', marginTop: 6, lineHeight: 1.6 }}>
|
||||
支持 MP3 / M4A / WAV / MP4,人声干净无杂音,时长 10 秒 ~ 2 分钟,不超过 20MB
|
||||
<div>支持 MP3 / M4A / WAV / MP4</div>
|
||||
<div>人声干净无杂音,时长 10 秒 ~ 2 分钟,不超过 50MB</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -102,6 +102,9 @@ export default function CoverDesign() {
|
||||
const [modalBgs, setModalBgs] = useState<BgImage[]>([]);
|
||||
const [lastModalBgIds, setLastModalBgIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// 视频原始分辨率(用于封面按目标分辨率渲染)
|
||||
const [videoResolution, setVideoResolution] = useState<{ width: number; height: number } | null>(null);
|
||||
|
||||
const { canvasRef, initCanvas, renderCover, exportPng } = useCoverFabric();
|
||||
|
||||
const { checkBalance } = usePointsCheck();
|
||||
@@ -259,13 +262,43 @@ export default function CoverDesign() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.backgroundImage, config.avatarImage, projectId]);
|
||||
|
||||
// 探测视频分辨率(优先从成品视频,其次从人物形象素材)
|
||||
useEffect(() => {
|
||||
const detect = async () => {
|
||||
const state = useProjectStore.getState();
|
||||
const pathsToTry = [
|
||||
state.composedVideoPath,
|
||||
state.avatarMaterialPath,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const path of pathsToTry) {
|
||||
try {
|
||||
const res = await invoke<{ code: number; data?: { width: number; height: number; duration: number; fps: number }; message: string }>('get_video_metadata_cmd', {
|
||||
request: { path },
|
||||
});
|
||||
if (res.code === 200 && res.data) {
|
||||
setVideoResolution({ width: res.data.width, height: res.data.height });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// 继续尝试下一个
|
||||
}
|
||||
}
|
||||
// 全部探测失败,使用默认 1080p
|
||||
setVideoResolution({ width: 1080, height: 1920 });
|
||||
};
|
||||
detect();
|
||||
}, []);
|
||||
|
||||
// 配置变化时重新渲染 Canvas
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
renderCover(config);
|
||||
const tw = videoResolution?.width ?? 1080;
|
||||
const th = videoResolution?.height ?? 1920;
|
||||
renderCover(config, tw, th);
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, [config, renderCover]);
|
||||
}, [config, renderCover, videoResolution]);
|
||||
|
||||
// 生成封面图
|
||||
const handleGenerate = async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
min-height: 38px;
|
||||
min-height: 64px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
|
||||
@@ -188,47 +188,56 @@ export default function SubtitleBurning() {
|
||||
// 构建 ASS Style 参数
|
||||
const videoDurationMs = (alignment?.duration || 0) * 1000;
|
||||
|
||||
const buildSubtitleStyle = (preset: TitlePreset, scale: number = 1): Partial<AssStyle> => ({
|
||||
fontSize: Math.round(64 * scale),
|
||||
primaryColor: htmlColorToAss(preset.primaryColor),
|
||||
outlineColor: htmlColorToAss(preset.outlineColor),
|
||||
backColor: htmlColorToAss(preset.backColor),
|
||||
borderStyle: preset.borderStyle,
|
||||
outline: Math.round(preset.outline * scale),
|
||||
shadow: 0,
|
||||
alignment: 2,
|
||||
marginV: Math.round(480 * scale),
|
||||
marginL: Math.round(160 * scale),
|
||||
marginR: Math.round(160 * scale),
|
||||
});
|
||||
const buildSubtitleStyle = (preset: TitlePreset, scale: number = 1): Partial<AssStyle> => {
|
||||
const is720 = scale <= 0.7;
|
||||
return {
|
||||
fontSize: is720 ? 40 : 64,
|
||||
primaryColor: htmlColorToAss(preset.primaryColor),
|
||||
outlineColor: htmlColorToAss(preset.outlineColor),
|
||||
backColor: htmlColorToAss(preset.backColor),
|
||||
borderStyle: preset.borderStyle,
|
||||
outline: Math.round(preset.outline * scale),
|
||||
shadow: 0,
|
||||
alignment: 2,
|
||||
marginV: Math.round(480 * scale),
|
||||
marginL: Math.round(160 * scale),
|
||||
marginR: Math.round(160 * scale),
|
||||
};
|
||||
};
|
||||
|
||||
const buildMainTitleStyle = (preset: TitlePreset, scale: number = 1): Partial<AssStyle> => ({
|
||||
fontSize: Math.round(96 * scale),
|
||||
primaryColor: htmlColorToAss(preset.primaryColor),
|
||||
outlineColor: htmlColorToAss(preset.outlineColor),
|
||||
backColor: htmlColorToAss(preset.backColor),
|
||||
borderStyle: preset.borderStyle,
|
||||
outline: Math.round(preset.outline * scale),
|
||||
shadow: 0,
|
||||
alignment: 8,
|
||||
marginV: Math.round(320 * scale),
|
||||
marginL: Math.round(160 * scale),
|
||||
marginR: Math.round(160 * scale),
|
||||
});
|
||||
const buildMainTitleStyle = (preset: TitlePreset, scale: number = 1): Partial<AssStyle> => {
|
||||
const is720 = scale <= 0.7;
|
||||
return {
|
||||
fontSize: is720 ? 64 : 90,
|
||||
primaryColor: htmlColorToAss(preset.primaryColor),
|
||||
outlineColor: htmlColorToAss(preset.outlineColor),
|
||||
backColor: htmlColorToAss(preset.backColor),
|
||||
borderStyle: preset.borderStyle,
|
||||
outline: Math.round(preset.outline * scale),
|
||||
shadow: 0,
|
||||
alignment: 8,
|
||||
marginV: Math.round(320 * scale),
|
||||
marginL: Math.round(160 * scale),
|
||||
marginR: Math.round(160 * scale),
|
||||
};
|
||||
};
|
||||
|
||||
const buildSubTitleStyle = (preset: TitlePreset, scale: number = 1): Partial<AssStyle> => ({
|
||||
fontSize: Math.round(80 * scale),
|
||||
primaryColor: htmlColorToAss(preset.primaryColor),
|
||||
outlineColor: htmlColorToAss(preset.outlineColor),
|
||||
backColor: htmlColorToAss(preset.backColor),
|
||||
borderStyle: preset.borderStyle,
|
||||
outline: Math.round(preset.outline * scale),
|
||||
shadow: 0,
|
||||
alignment: 8,
|
||||
marginV: Math.round(440 * scale),
|
||||
marginL: Math.round(160 * scale),
|
||||
marginR: Math.round(160 * scale),
|
||||
});
|
||||
const buildSubTitleStyle = (preset: TitlePreset, scale: number = 1): Partial<AssStyle> => {
|
||||
const is720 = scale <= 0.7;
|
||||
return {
|
||||
fontSize: is720 ? 50 : 72,
|
||||
primaryColor: htmlColorToAss(preset.primaryColor),
|
||||
outlineColor: htmlColorToAss(preset.outlineColor),
|
||||
backColor: htmlColorToAss(preset.backColor),
|
||||
borderStyle: preset.borderStyle,
|
||||
outline: Math.round(preset.outline * scale),
|
||||
shadow: 0,
|
||||
alignment: 8,
|
||||
marginV: Math.round(440 * scale),
|
||||
marginL: Math.round(160 * scale),
|
||||
marginR: Math.round(160 * scale),
|
||||
};
|
||||
};
|
||||
|
||||
// 用 text-shadow 模拟 ASS 外描边(比 WebkitTextStroke 的居中描边更准确)
|
||||
const getOutlineShadow = (color: string, outlineWidth: number): string => {
|
||||
@@ -385,13 +394,21 @@ export default function SubtitleBurning() {
|
||||
const mtStyle = mtPreset ? buildMainTitleStyle(mtPreset) : undefined;
|
||||
const stStyle = stPreset ? buildSubTitleStyle(stPreset) : undefined;
|
||||
|
||||
// 2. 确定目标分辨率(优先复用组件 state 中已探测的结果)
|
||||
let targetWidth = videoResolution?.width ?? 1080;
|
||||
let targetHeight = videoResolution?.height ?? 1920;
|
||||
|
||||
if ((mainTitle && mtStyle) || (subTitle && stStyle)) {
|
||||
const { generateTitlePngDataUrl } = await import('../../utils/titlePngGenerator');
|
||||
// 标题 PNG 使用独立的字体大小(与 ASS 不同)
|
||||
const titlePngMainStyle = mtStyle ? { ...mtStyle, fontSize: targetWidth === 720 ? 64 : 104 } : undefined;
|
||||
const pngDataUrl = await generateTitlePngDataUrl(
|
||||
mainTitle || undefined,
|
||||
subTitle || undefined,
|
||||
mtStyle,
|
||||
titlePngMainStyle,
|
||||
stStyle,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
);
|
||||
// 去掉 data:image/png;base64, 前缀
|
||||
const base64 = pngDataUrl.replace(/^data:image\/png;base64,/, '');
|
||||
@@ -406,9 +423,6 @@ export default function SubtitleBurning() {
|
||||
overlayImagePath = pngSaveRes.data;
|
||||
}
|
||||
|
||||
// 2. 确定 ASS 字幕目标分辨率(优先复用组件 state 中已探测的结果)
|
||||
let targetWidth = videoResolution?.width ?? 1080;
|
||||
let targetHeight = videoResolution?.height ?? 1920;
|
||||
if (!videoResolution) {
|
||||
const metaRes = await invoke<{ code: number; data?: { width: number; height: number; duration: number; fps: number }; message: string }>('get_video_metadata_cmd', {
|
||||
request: { path: actualVideoPath },
|
||||
|
||||
@@ -13,6 +13,10 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { client, clearAuthCache, PYTHON_API_BASE_URL, setOnTokenRefreshed, setOnAuthFailed } from '../api/client';
|
||||
import { isTauri } from '../utils/env';
|
||||
import { useVoiceStore } from './voiceStore';
|
||||
import { useCoverAvatarStore } from './coverAvatarStore';
|
||||
import { useProgressStore } from './progressStore';
|
||||
import { useProjectStore } from './projectStore';
|
||||
// uiStore 不再直接导入,弹窗由 React 组件通过状态驱动渲染
|
||||
|
||||
interface UserInfo {
|
||||
@@ -283,10 +287,10 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
set({ ...initialState, isLoading: false, showKickModal: false, kickMessage: '' });
|
||||
// 清除其他 Store 的内存状态,防止被踢后重新登录时数据残留
|
||||
try {
|
||||
import('./voiceStore').then(m => m.useVoiceStore.getState().reset());
|
||||
import('./coverAvatarStore').then(m => m.useCoverAvatarStore.getState().reset());
|
||||
import('./progressStore').then(m => m.useProgressStore.getState().reset());
|
||||
import('./projectStore').then(m => m.useProjectStore.getState().reset());
|
||||
useVoiceStore.getState().reset();
|
||||
useCoverAvatarStore.getState().reset();
|
||||
useProgressStore.getState().reset();
|
||||
useProjectStore.getState().reset();
|
||||
} catch (e) {
|
||||
console.error('[Auth] 清除其他 Store 状态失败:', e);
|
||||
}
|
||||
@@ -320,10 +324,6 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
|
||||
// 清除其他 Store 的内存状态,防止切换账号后数据残留
|
||||
try {
|
||||
const { useVoiceStore } = await import('./voiceStore');
|
||||
const { useCoverAvatarStore } = await import('./coverAvatarStore');
|
||||
const { useProgressStore } = await import('./progressStore');
|
||||
const { useProjectStore } = await import('./projectStore');
|
||||
useVoiceStore.getState().reset();
|
||||
useCoverAvatarStore.getState().reset();
|
||||
useProgressStore.getState().reset();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { create } from 'zustand';
|
||||
import type { VoiceInfo, VoiceMaterial } from '../api/modules/voice';
|
||||
import * as voiceApi from '../api/modules/voice';
|
||||
import { writeFile, remove, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||
import { writeFile, remove, mkdir, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||
import { join, appLocalDataDir } from '@tauri-apps/api/path';
|
||||
|
||||
interface VoiceState {
|
||||
@@ -136,33 +136,45 @@ export const useVoiceStore = create<VoiceState & VoiceActions>()(
|
||||
|
||||
try {
|
||||
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
||||
console.log('[VoiceStore] addVoiceMaterial 开始, ext=', ext, 'fileName=', file.name, 'size=', file.size);
|
||||
|
||||
if (ext === '.mp4') {
|
||||
// MP4 需要先本地提取音频
|
||||
tempVideoPath = await join('temp', `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.mp4`);
|
||||
console.log('[VoiceStore] tempVideoPath=', tempVideoPath);
|
||||
|
||||
// 1a. 将 File 写入本地临时文件(使用 BaseDirectory.AppLocalData)
|
||||
console.log('[VoiceStore] 开始 writeFile...');
|
||||
await mkdir('temp', { baseDir: BaseDirectory.AppLocalData, recursive: true });
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
await writeFile(tempVideoPath, uint8Array, { baseDir: BaseDirectory.AppLocalData });
|
||||
console.log('[VoiceStore] writeFile 完成');
|
||||
|
||||
// 1b. 调用 FFmpeg 提取音频(Rust 返回绝对路径)
|
||||
tempMp3Path = await voiceApi.extractAudioFromVideo(
|
||||
await join(await appLocalDataDir(), tempVideoPath)
|
||||
);
|
||||
const videoAbsPath = await join(await appLocalDataDir(), tempVideoPath);
|
||||
console.log('[VoiceStore] 开始 extractAudioFromVideo, path=', videoAbsPath);
|
||||
tempMp3Path = await voiceApi.extractAudioFromVideo(videoAbsPath);
|
||||
console.log('[VoiceStore] extractAudioFromVideo 完成, mp3Path=', tempMp3Path);
|
||||
|
||||
// 1c. 上传提取的 MP3
|
||||
console.log('[VoiceStore] 开始 uploadLocalAudioFile...');
|
||||
sourceUrl = await voiceApi.uploadLocalAudioFile(tempMp3Path);
|
||||
console.log('[VoiceStore] uploadLocalAudioFile 完成, url=', sourceUrl);
|
||||
} else {
|
||||
// 音频文件直接上传
|
||||
console.log('[VoiceStore] 直接上传音频文件...');
|
||||
sourceUrl = await voiceApi.uploadAudio(file);
|
||||
console.log('[VoiceStore] uploadAudio 完成, url=', sourceUrl);
|
||||
}
|
||||
|
||||
// 2. 提交 Vidu 同步克隆
|
||||
console.log('[VoiceStore] 开始 submitCloneTask...');
|
||||
const cloneResult = await voiceApi.submitCloneTask({
|
||||
sourceAudioUrl: sourceUrl,
|
||||
voiceName: name,
|
||||
});
|
||||
console.log('[VoiceStore] submitCloneTask 完成, result=', cloneResult);
|
||||
|
||||
// 3. Vidu 同步返回,直接 ready
|
||||
const material: VoiceMaterial = {
|
||||
@@ -174,26 +186,36 @@ export const useVoiceStore = create<VoiceState & VoiceActions>()(
|
||||
status: 'ready',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
console.log('[VoiceStore] material=', material);
|
||||
|
||||
// 4. 保存到本地 JSON
|
||||
console.log('[VoiceStore] 开始 saveVoiceMaterial...');
|
||||
await voiceApi.saveVoiceMaterial(material);
|
||||
console.log('[VoiceStore] saveVoiceMaterial 完成');
|
||||
set(state => ({ voiceMaterials: [material, ...state.voiceMaterials] }));
|
||||
|
||||
console.log('[VoiceStore] addVoiceMaterial 成功返回');
|
||||
return material;
|
||||
} catch (err) {
|
||||
console.error('[VoiceStore] addVoiceMaterial 异常:', err, '类型:', typeof err, '是Error?', err instanceof Error, 'message:', (err as Error)?.message);
|
||||
throw err;
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
console.log('[VoiceStore] finally 清理, tempVideoPath=', tempVideoPath, 'tempMp3Path=', tempMp3Path);
|
||||
if (tempVideoPath) {
|
||||
try {
|
||||
await remove(tempVideoPath, { baseDir: BaseDirectory.AppLocalData });
|
||||
} catch {
|
||||
// 忽略清理错误
|
||||
console.log('[VoiceStore] 清理 tempVideoPath 成功');
|
||||
} catch (e) {
|
||||
console.error('[VoiceStore] 清理 tempVideoPath 失败:', e);
|
||||
}
|
||||
}
|
||||
if (tempMp3Path) {
|
||||
try {
|
||||
await remove(tempMp3Path);
|
||||
} catch {
|
||||
// 忽略清理错误
|
||||
await remove(tempMp3Path, { baseDir: BaseDirectory.AppLocalData });
|
||||
console.log('[VoiceStore] 清理 tempMp3Path 成功');
|
||||
} catch (e) {
|
||||
console.error('[VoiceStore] 清理 tempMp3Path 失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,10 @@
|
||||
import type { AssStyle } from './assGenerator';
|
||||
import { setCanvasFont, drawTextBlock, loadCustomFont } from './canvasSubtitleDrawer';
|
||||
|
||||
const VIDEO_WIDTH = 1080;
|
||||
const VIDEO_HEIGHT = 1920;
|
||||
|
||||
/**
|
||||
* 生成标题 PNG DataURL
|
||||
*
|
||||
* Canvas 尺寸与视频一致(1080x1920),只在标题区域绘制内容,
|
||||
* Canvas 尺寸与视频分辨率一致,只在标题区域绘制内容,
|
||||
* 其余部分透明。FFmpeg overlay 时直接 x=0:y=0 叠加即可。
|
||||
*/
|
||||
export async function generateTitlePngDataUrl(
|
||||
@@ -23,13 +20,15 @@ export async function generateTitlePngDataUrl(
|
||||
subTitle: string | undefined,
|
||||
mainTitleStyle: Partial<AssStyle> | undefined,
|
||||
subTitleStyle: Partial<AssStyle> | undefined,
|
||||
targetWidth: number = 1080,
|
||||
targetHeight: number = 1920,
|
||||
): Promise<string> {
|
||||
// 确保字体已加载
|
||||
await loadCustomFont();
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = VIDEO_WIDTH;
|
||||
canvas.height = VIDEO_HEIGHT;
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
@@ -39,30 +38,31 @@ export async function generateTitlePngDataUrl(
|
||||
// 清空为透明
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const scale = 1; // 1:1,Canvas 尺寸就是视频分辨率
|
||||
const baseWidth = 1080;
|
||||
const scale = targetWidth / baseWidth;
|
||||
|
||||
// ---- 绘制大标题(单行,不换行) ----
|
||||
// ASS alignment=8: marginV 是文字块顶部到画面顶部的距离
|
||||
// Canvas fillText y 是基线,基线 = 顶部 + ascent
|
||||
if (mainTitle && mainTitleStyle) {
|
||||
setCanvasFont(ctx, mainTitleStyle, scale);
|
||||
const marginV = mainTitleStyle.marginV ?? 360;
|
||||
const fontSize = mainTitleStyle.fontSize ?? 104;
|
||||
const marginV = (mainTitleStyle.marginV ?? 360) * scale;
|
||||
const fontSize = (mainTitleStyle.fontSize ?? 104) * scale;
|
||||
const metrics = ctx.measureText(mainTitle);
|
||||
const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.72;
|
||||
const startY = marginV + ascent;
|
||||
drawTextBlock(ctx, mainTitle, VIDEO_WIDTH / 2, startY, mainTitleStyle, scale, VIDEO_WIDTH, 1);
|
||||
drawTextBlock(ctx, mainTitle, targetWidth / 2, startY, mainTitleStyle, scale, targetWidth, 1);
|
||||
}
|
||||
|
||||
// ---- 绘制小标题(单行,不换行) ----
|
||||
if (subTitle && subTitleStyle) {
|
||||
setCanvasFont(ctx, subTitleStyle, scale);
|
||||
const marginV = subTitleStyle.marginV ?? 480;
|
||||
const fontSize = subTitleStyle.fontSize ?? 88;
|
||||
const marginV = (subTitleStyle.marginV ?? 480) * scale;
|
||||
const fontSize = (subTitleStyle.fontSize ?? 88) * scale;
|
||||
const metrics = ctx.measureText(subTitle);
|
||||
const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.72;
|
||||
const startY = marginV + ascent;
|
||||
drawTextBlock(ctx, subTitle, VIDEO_WIDTH / 2, startY, subTitleStyle, scale, VIDEO_WIDTH, 1);
|
||||
drawTextBlock(ctx, subTitle, targetWidth / 2, startY, subTitleStyle, scale, targetWidth, 1);
|
||||
}
|
||||
|
||||
return canvas.toDataURL('image/png');
|
||||
|
||||
Reference in New Issue
Block a user