fix: restore deploy manifests and polish script assistant
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
This commit is contained in:
parent
df7b90934a
commit
e06a16e200
15
k8s/cert-manager-issuer.yaml
Normal file
15
k8s/cert-manager-issuer.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# ClusterIssuer for Let's Encrypt automatic certificate generation & renewal
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: airlabsv001@gmail.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
24
k8s/ingress.yaml
Normal file
24
k8s/ingress.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: airshelf-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "traefik"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: "default-redirect-https@kubernetescrd"
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- airshelf.airlabs.art
|
||||
secretName: airshelf-tls
|
||||
rules:
|
||||
- host: airshelf.airlabs.art
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: airshelf-web
|
||||
port:
|
||||
number: 80
|
||||
8
k8s/redirect-https-middleware.yaml
Normal file
8
k8s/redirect-https-middleware.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: redirect-https
|
||||
spec:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
59
k8s/web-deployment.yaml
Normal file
59
k8s/web-deployment.yaml
Normal file
@ -0,0 +1,59 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: airshelf-web
|
||||
labels:
|
||||
app: airshelf-web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: airshelf-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: airshelf-web
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: cr-pull-secret
|
||||
containers:
|
||||
- name: airshelf-web
|
||||
image: ${CI_REGISTRY_IMAGE}/airshelf-web:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "20m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "150m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: airshelf-web
|
||||
spec:
|
||||
selector:
|
||||
app: airshelf-web
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
@ -438,6 +438,12 @@
|
||||
}
|
||||
|
||||
/* 镜头脚本顶栏 · 自动从脚本抓取的人物/场景标签 · 可编辑/删除/添加 */
|
||||
.shot-list > .pane-h { flex-wrap: wrap; row-gap: 8px; }
|
||||
.shot-headline { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.script-brief-summary { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
|
||||
.script-brief-pill { gap: 4px; padding: 3px 8px; font-size: 11px; }
|
||||
.script-brief-pill .k { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||||
.script-brief-pill .v { color: var(--accent-black); max-width: 116px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.script-tags { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 14px; margin-left: 6px; }
|
||||
.script-tags .tag-group { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.script-tags .tg-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; flex-shrink: 0; }
|
||||
@ -465,6 +471,61 @@
|
||||
.chat-mode:hover { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }
|
||||
.chat-mode.primary { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }
|
||||
.chat-mode svg { width: 13px; height: 13px; }
|
||||
.script-brief-card { margin-top: 8px; padding: 12px; background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: flex; flex-direction: column; gap: 10px; }
|
||||
.script-brief-row { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 10px; row-gap: 4px; align-items: center; }
|
||||
.script-brief-row .k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||||
.script-brief-row .why { grid-column: 2 / 3; font-size: 11.5px; color: var(--black-alpha-48); line-height: 1.5; }
|
||||
.script-brief-select { position: relative; display: inline-flex; width: 100%; min-width: 0; }
|
||||
.script-brief-value {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--surface);
|
||||
color: var(--accent-black);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.script-brief-value .v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
|
||||
.script-brief-value::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
transform: rotate(45deg) translateY(-1px);
|
||||
transition: transform var(--t-base);
|
||||
color: var(--black-alpha-48);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.script-brief-value:hover {
|
||||
background: var(--heat-12);
|
||||
border-color: var(--heat-20);
|
||||
color: var(--heat);
|
||||
}
|
||||
.script-brief-select.open .script-brief-value {
|
||||
background: var(--heat-12);
|
||||
border-color: var(--heat);
|
||||
color: var(--heat);
|
||||
}
|
||||
.script-brief-select.open .script-brief-value::after {
|
||||
color: var(--heat);
|
||||
transform: rotate(225deg) translate(-1px, -1px);
|
||||
}
|
||||
.script-brief-select.open .chip-menu { display: block; }
|
||||
.script-brief-select .chip-menu { min-width: 168px; right: 0; left: auto; z-index: 80; }
|
||||
.script-brief-select .chip-menu .mi { width: 100%; border: 0; background: transparent; font-family: inherit; text-align: left; }
|
||||
.script-brief-actions { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 10px; align-items: center; padding-top: 2px; }
|
||||
.script-brief-actions .action-row { grid-column: 2 / 3; display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
||||
|
||||
/* AI 思考态 typing indicator */
|
||||
.ai-thinking .dots { display: inline-flex; gap: 3px; }
|
||||
@ -1626,8 +1687,15 @@
|
||||
<div class="stage-script">
|
||||
<div class="pane shot-list">
|
||||
<div class="pane-h">
|
||||
<div class="shot-headline">
|
||||
<strong>镜头脚本</strong>
|
||||
<span class="muted-2 mono" id="shots-meta" style="font-size:11px;">· 空 · 待生成</span>
|
||||
</div>
|
||||
<div class="script-brief-summary" aria-label="当前创作方向">
|
||||
<span class="pill neutral script-brief-pill"><span class="k">来源</span><span class="v" id="brief-source">未选择</span></span>
|
||||
<span class="pill neutral script-brief-pill"><span class="k">风格</span><span class="v" id="brief-style">待确认</span></span>
|
||||
<span class="pill neutral script-brief-pill"><span class="k">人物</span><span class="v" id="brief-persona">待确认</span></span>
|
||||
</div>
|
||||
<div class="script-tags" id="script-tags">
|
||||
<div class="tag-group" data-kind="char">
|
||||
<span class="tg-lbl">// 人物</span>
|
||||
@ -1662,7 +1730,7 @@
|
||||
<div class="chat-input">
|
||||
<div class="chat-input-card">
|
||||
<div class="chat-attach-row" id="chat-attach-row" hidden></div>
|
||||
<textarea class="chat-input-area" id="chat-textarea" placeholder='聊聊你的脚本想法,或输入 "@" 引用镜头……' rows="2"></textarea>
|
||||
<textarea class="chat-input-area" id="chat-textarea" placeholder="直接说怎么改,如:更像小红书种草 / 换成熬夜党" rows="2"></textarea>
|
||||
<div class="chat-input-foot">
|
||||
<button class="chat-icon-btn" id="chat-upload-btn" title="上传脚本附件" aria-label="上传脚本附件">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
@ -2802,10 +2870,177 @@ const Stage1 = (function () {
|
||||
let chatMsgs = []; // [{ role, html, time }]
|
||||
let mode = null;
|
||||
let scriptTags = { char: [], scene: [] }; // 自动抓取的人物 / 场景 · 可编辑
|
||||
const MODE_LABELS = { ai: 'AI 全生', theme: '一句话主题', manual: '自带脚本' };
|
||||
const MODE_USER_COPY = {
|
||||
ai: '我用 AI 全生',
|
||||
theme: '我用一句话主题',
|
||||
manual: '我用自带脚本',
|
||||
};
|
||||
const BRIEF_OPTIONS = {
|
||||
style: ['真实测评', '痛点种草', '小红书种草', '开箱测评', '对比展示'],
|
||||
persona: ['通勤敏感肌女生', '熬夜党通勤女性', '学生党女生', '精致宝妈', '成分党用户'],
|
||||
};
|
||||
function makeBrief(nextMode) {
|
||||
if (!nextMode) {
|
||||
return {
|
||||
source: '未选择',
|
||||
style: '待确认',
|
||||
persona: '待确认',
|
||||
styleNote: '选择脚本来源后由助手推荐',
|
||||
personaNote: '选择脚本来源后由助手推荐',
|
||||
};
|
||||
}
|
||||
return {
|
||||
source: MODE_LABELS[nextMode] || '未选择',
|
||||
style: nextMode === 'theme' ? '痛点种草' : '真实测评',
|
||||
persona: '通勤敏感肌女生',
|
||||
styleNote: nextMode === 'manual' ? '参考文本识别不足 · 根据商品类目推荐' : '根据商品信息推荐',
|
||||
personaNote: '根据商品目标人群推荐',
|
||||
};
|
||||
}
|
||||
let scriptBrief = makeBrief(null);
|
||||
|
||||
const $cb = () => document.getElementById('chat-body');
|
||||
const $sb = () => document.getElementById('shots-body');
|
||||
const $sm = () => document.getElementById('shots-meta');
|
||||
const safeHtml = s => String(s || '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]));
|
||||
function renderBriefSummary() {
|
||||
const source = document.getElementById('brief-source');
|
||||
const style = document.getElementById('brief-style');
|
||||
const persona = document.getElementById('brief-persona');
|
||||
if (source) source.textContent = scriptBrief.source || '未选择';
|
||||
if (style) style.textContent = scriptBrief.style || '待确认';
|
||||
if (persona) persona.textContent = scriptBrief.persona || '待确认';
|
||||
}
|
||||
function briefConfirmHtml(intro) {
|
||||
const optionMenu = (kind, current) => `
|
||||
<span class="script-brief-select">
|
||||
<button class="script-brief-value" type="button" data-brief-act="${kind}" aria-haspopup="listbox" aria-expanded="false" aria-label="选择${kind === 'style' ? '脚本风格' : '人物设定'}">
|
||||
<span class="v">${safeHtml(current)}</span>
|
||||
</button>
|
||||
<span class="chip-menu align-right" role="listbox">
|
||||
${BRIEF_OPTIONS[kind].map(opt => `
|
||||
<button class="mi${opt === current ? ' selected' : ''}" type="button" data-brief-pick="${kind}" data-value="${safeHtml(opt)}" role="option" aria-selected="${opt === current ? 'true' : 'false'}">
|
||||
<svg class="mi-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m20 6-11 11-5-5"/></svg>
|
||||
${safeHtml(opt)}
|
||||
</button>`).join('')}
|
||||
</span>
|
||||
</span>`;
|
||||
return `${intro}
|
||||
<div class="script-brief-card">
|
||||
<div class="script-brief-row">
|
||||
<span class="k">风格</span>
|
||||
${optionMenu('style', scriptBrief.style)}
|
||||
<span class="why">${safeHtml(scriptBrief.styleNote)}</span>
|
||||
</div>
|
||||
<div class="script-brief-row">
|
||||
<span class="k">人物</span>
|
||||
${optionMenu('persona', scriptBrief.persona)}
|
||||
<span class="why">${safeHtml(scriptBrief.personaNote)}</span>
|
||||
</div>
|
||||
<div class="script-brief-actions">
|
||||
<div class="action-row">
|
||||
<button class="btn btn-ghost" type="button" data-brief-act="reroll">重新推荐</button>
|
||||
<button class="btn" type="button" data-brief-act="accept">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
function migrateBriefMessages() {
|
||||
let changed = false;
|
||||
chatMsgs = chatMsgs.map(msg => {
|
||||
if (!msg || typeof msg.html !== 'string') return msg;
|
||||
if (msg.role === 'ai' && /script-brief-card/.test(msg.html)) {
|
||||
const cardIndex = msg.html.indexOf('<div class="script-brief-card"');
|
||||
const intro = cardIndex > -1 ? msg.html.slice(0, cardIndex).trim() : '';
|
||||
changed = true;
|
||||
return {
|
||||
...msg,
|
||||
html: briefConfirmHtml(intro || '已更新创作方向。确认后我会按这个方向重写镜头脚本。'),
|
||||
};
|
||||
}
|
||||
if (msg.role === 'user' && msg.html === '保持推荐,生成镜头脚本') {
|
||||
changed = true;
|
||||
return { ...msg, html: '确定' };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
function tuneBriefByText(text) {
|
||||
if (/小红书|种草/.test(text)) scriptBrief.style = '小红书种草';
|
||||
else if (/测评|评测/.test(text)) scriptBrief.style = '真实测评';
|
||||
else if (/痛点/.test(text)) scriptBrief.style = '痛点种草';
|
||||
if (/熬夜/.test(text)) scriptBrief.persona = '熬夜党通勤女性';
|
||||
else if (/学生/.test(text)) scriptBrief.persona = '学生党女生';
|
||||
else if (/宝妈|妈妈/.test(text)) scriptBrief.persona = '精致宝妈';
|
||||
}
|
||||
function startScriptGeneration() {
|
||||
ProjectStore.startJob('stage1-script', {
|
||||
stage: 1,
|
||||
label: '脚本初稿生成',
|
||||
finishAt: Date.now() + 6500,
|
||||
});
|
||||
if (!chatMsgs.some(x => /正在解析商品卖点/.test(x.html))) {
|
||||
pushMsg('ai', '<span class="ai-thinking">正在解析商品卖点与创作方向 <span class="dots"><span></span><span></span><span></span></span></span>');
|
||||
}
|
||||
ProjectStore.record('stage1.script.generate', { mode, brief: scriptBrief });
|
||||
saveState();
|
||||
renderChat();
|
||||
window.setTimeout(completeAiJob, 6500);
|
||||
}
|
||||
function refreshLatestBriefMessage(intro) {
|
||||
for (let i = chatMsgs.length - 1; i >= 0; i--) {
|
||||
if (chatMsgs[i].role === 'ai' && /script-brief-card/.test(chatMsgs[i].html)) {
|
||||
chatMsgs[i].html = briefConfirmHtml(intro);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pushMsg('ai', briefConfirmHtml(intro));
|
||||
}
|
||||
function handleBriefPick(kind, value) {
|
||||
if (!value) return;
|
||||
if (kind === 'style') {
|
||||
scriptBrief.style = value;
|
||||
scriptBrief.styleNote = '已手动选择';
|
||||
} else if (kind === 'persona') {
|
||||
scriptBrief.persona = value;
|
||||
scriptBrief.personaNote = '已手动选择';
|
||||
}
|
||||
refreshLatestBriefMessage('已更新创作方向。确认后我会按这个方向重写镜头脚本。');
|
||||
ProjectStore.record('stage1.brief.option.selected', { kind, value });
|
||||
saveState();
|
||||
renderChat();
|
||||
}
|
||||
function handleBriefAction(action, trigger) {
|
||||
if (action === 'accept') {
|
||||
pushMsg('user', '确定');
|
||||
startScriptGeneration();
|
||||
} else if (action === 'reroll') {
|
||||
scriptBrief = {
|
||||
...scriptBrief,
|
||||
style: scriptBrief.style === '真实测评' ? '痛点种草' : '真实测评',
|
||||
persona: scriptBrief.persona === '通勤敏感肌女生' ? '熬夜党通勤女性' : '通勤敏感肌女生',
|
||||
styleNote: '已根据商品卖点重新推荐',
|
||||
personaNote: '已根据使用场景重新推荐',
|
||||
};
|
||||
pushMsg('ai', briefConfirmHtml('我重新给你配了一组创作方向,确认后就生成镜头脚本。'));
|
||||
saveState();
|
||||
renderChat();
|
||||
} else if (action === 'style') {
|
||||
const box = trigger?.closest('.script-brief-select');
|
||||
if (!box) return;
|
||||
document.querySelectorAll('.script-brief-select.open').forEach(el => { if (el !== box) el.classList.remove('open'); });
|
||||
box.classList.toggle('open');
|
||||
trigger.setAttribute('aria-expanded', box.classList.contains('open') ? 'true' : 'false');
|
||||
} else if (action === 'persona') {
|
||||
const box = trigger?.closest('.script-brief-select');
|
||||
if (!box) return;
|
||||
document.querySelectorAll('.script-brief-select.open').forEach(el => { if (el !== box) el.classList.remove('open'); });
|
||||
box.classList.toggle('open');
|
||||
trigger.setAttribute('aria-expanded', box.classList.contains('open') ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/* 自动从脚本(painting + dialog 文本)抽取人物 / 场景关键词 · 白名单匹配 */
|
||||
const CHAR_KEYWORDS = ['女主', '男主', '同事', '闺蜜', '男友', '女友', '妈妈', '爸爸', '老师', '同学', '朋友', '路人', '主播', '老板'];
|
||||
@ -2872,17 +3107,24 @@ const Stage1 = (function () {
|
||||
}
|
||||
function pushMsg(role, html) { chatMsgs.push({ role, html, time: now() }); }
|
||||
function saveState() {
|
||||
ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags });
|
||||
ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags, scriptBrief });
|
||||
renderBriefSummary();
|
||||
}
|
||||
function loadState() {
|
||||
const saved = ProjectStore.data.stage1;
|
||||
if (!saved) return;
|
||||
if (!saved) { renderBriefSummary(); return; }
|
||||
if (Array.isArray(saved.shots)) shots = saved.shots;
|
||||
if (Array.isArray(saved.chatMsgs)) chatMsgs = saved.chatMsgs;
|
||||
if (saved.mode) mode = saved.mode;
|
||||
if (saved.scriptBrief && typeof saved.scriptBrief === 'object') scriptBrief = { ...scriptBrief, ...saved.scriptBrief };
|
||||
else if (mode) scriptBrief = makeBrief(mode);
|
||||
if (saved.scriptTags && Array.isArray(saved.scriptTags.char) && Array.isArray(saved.scriptTags.scene)) {
|
||||
scriptTags = saved.scriptTags;
|
||||
}
|
||||
if (migrateBriefMessages()) {
|
||||
ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags, scriptBrief });
|
||||
}
|
||||
renderBriefSummary();
|
||||
}
|
||||
function getDefaultDraft() {
|
||||
return [
|
||||
@ -2945,6 +3187,18 @@ const Stage1 = (function () {
|
||||
}
|
||||
return `<div class="msg user"><div class="bubble">${msg.html}</div><div class="time">${msg.time}</div></div>`;
|
||||
}).join('');
|
||||
body.querySelectorAll('[data-brief-act]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
handleBriefAction(btn.dataset.briefAct, btn);
|
||||
});
|
||||
});
|
||||
body.querySelectorAll('[data-brief-pick]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
handleBriefPick(btn.dataset.briefPick, btn.dataset.value);
|
||||
});
|
||||
});
|
||||
body.scrollTop = body.scrollHeight;
|
||||
}
|
||||
|
||||
@ -3030,62 +3284,19 @@ const Stage1 = (function () {
|
||||
|
||||
function pickMode(m) {
|
||||
mode = m;
|
||||
scriptBrief = makeBrief(m);
|
||||
ProjectStore.record('stage1.mode.selected', { mode: m });
|
||||
pushMsg('user', MODE_USER_COPY[m] || '选择脚本来源');
|
||||
if (m === 'ai') {
|
||||
ProjectStore.startJob('stage1-script', {
|
||||
stage: 1,
|
||||
label: '脚本初稿生成',
|
||||
finishAt: Date.now() + 6500,
|
||||
});
|
||||
pushMsg('user', '帮我 AI 全自动生成一稿脚本');
|
||||
saveState();
|
||||
renderChat();
|
||||
setTimeout(() => {
|
||||
pushMsg('ai', '<span class="ai-thinking">正在解析商品卖点与目标人群 <span class="dots"><span></span><span></span><span></span></span></span>');
|
||||
saveState();
|
||||
renderChat();
|
||||
}, 300);
|
||||
// 7 镜 · 0-40s · 与 Stage 2 / 4 的 3 场切分对齐(场 1 深夜办公桌 0-15s / 场 2 面膜包装 15-27s / 场 3 化妆台定格 27-40s)
|
||||
const draft = [
|
||||
// ─ 场 1 · 深夜办公桌(15s)─
|
||||
{ id: 'sh1', painting: '中景慢推 · 深夜居家书桌全景。屏幕仍亮着 PPT,女主背影瘫在椅子上,屏幕冷光 + 台灯暖光对比。字幕"凌晨 02:14"淡入。', dialog: '(无台词 · BGM 渐起)', duration: 5 },
|
||||
{ id: 'sh2', painting: '近景 · 卫生间镜前。女主低头看脸,T 区起皮、暗沉特写,冷白灯偏惨。', dialog: '"做完这版稿又是凌晨两点……(叹气)脸已经不能看了。"', duration: 5 },
|
||||
{ id: 'sh3', painting: '俯拍特写 · 回到书桌,拉开抽屉。囤好的透真补水面膜露半角,手伸进去抽出一片。', dialog: '"还好抽屉里囤了透真玻尿酸面膜。"', duration: 5 },
|
||||
// ─ 场 2 · 面膜包装/特写(12s)─
|
||||
{ id: 'sh4', painting: '桌面微距特写 · 撕开锡纸包装的瞬间。30g 厚精华液缓缓滴落,面膜布展开,质地拉丝可见。', dialog: '"30g 一片,精华液比普通面膜厚整整三倍。"', duration: 6 },
|
||||
{ id: 'sh5', painting: '床头近景 · 女主敷好面膜闭眼躺下,台灯暖光打在脸侧。膜布贴合脸型,边缘服帖。', dialog: '"贴上去那一瞬间 —— 凉凉的,像把皮肤泡了一次澡。"', duration: 6 },
|
||||
// ─ 场 3 · 化妆台/产品定格(13s)─
|
||||
{ id: 'sh6', painting: '中景 · 第二天清晨化妆台。阳光透过窗帘,女主对镜上妆,皮肤透亮、粉底服帖。同事画外音"你最近用啥了"。', dialog: '"第二天脸是软的,粉底都不卡了。同事都跑来问。"', duration: 8 },
|
||||
{ id: 'sh7', painting: '平铺俯拍 · 桌面五片装产品 + 单片包装。价格 "618 · 5 片 ¥39.9" 弹出,购物车图标右下角浮现。', dialog: '"618 五片 39.9,自用送人都合适。链接放评论区。"', duration: 5 },
|
||||
];
|
||||
let cur = 0;
|
||||
const step = () => {
|
||||
if (cur >= draft.length) {
|
||||
// remove thinking msg
|
||||
chatMsgs = chatMsgs.filter(x => !(x.role === 'ai' && /ai-thinking/.test(x.html)));
|
||||
pushMsg('ai', '初稿完成。点击任意卡片文字可直接编辑;鼠标移到卡片之间会出现「+ 添加分镜」。');
|
||||
ProjectStore.finishJob('stage1-script');
|
||||
ProjectStore.record('stage1.script.ready', { shots: shots.length });
|
||||
saveState();
|
||||
renderChat();
|
||||
return;
|
||||
}
|
||||
shots.push(draft[cur++]);
|
||||
saveState();
|
||||
renderShots();
|
||||
setTimeout(step, 700);
|
||||
};
|
||||
setTimeout(step, 1100);
|
||||
pushMsg('ai', briefConfirmHtml('我会根据商品信息直接生成第一版。生成前先确认创作方向。'));
|
||||
} else if (m === 'theme') {
|
||||
pushMsg('ai', '好,请给我一句话主题(5–30 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>下面输入框直接打就行,我会按这句话扩成一稿镜头脚本。');
|
||||
saveState();
|
||||
renderChat();
|
||||
pushMsg('ai', '好,请给我一句话主题(5-30 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>我会先根据这句话补全风格和人物设定,再让你确认。');
|
||||
} else if (m === 'manual') {
|
||||
pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框,我会按场自然切分并适配商品卖点。');
|
||||
pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框。我会先识别风格和人物设定;识别不到的部分,再用商品信息补齐。');
|
||||
}
|
||||
saveState();
|
||||
renderChat();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadState();
|
||||
@ -3095,6 +3306,7 @@ const Stage1 = (function () {
|
||||
ProjectStore.restoreFields();
|
||||
document.getElementById('chat-clear-btn')?.addEventListener('click', () => {
|
||||
chatMsgs = []; mode = null; shots = []; scriptTags = { char: [], scene: [] };
|
||||
scriptBrief = makeBrief(null);
|
||||
ProjectStore.clearJob('stage1-script');
|
||||
ProjectStore.record('stage1.cleared');
|
||||
saveState();
|
||||
@ -3104,6 +3316,10 @@ const Stage1 = (function () {
|
||||
document.getElementById('chat-regen-btn')?.addEventListener('click', () => {
|
||||
Shell.toast('已请求整体重写', 'POST /script/regen');
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.script-brief-select')) return;
|
||||
document.querySelectorAll('.script-brief-select.open').forEach(el => el.classList.remove('open'));
|
||||
});
|
||||
const sendBtn = document.getElementById('chat-send-btn');
|
||||
const ta = document.getElementById('chat-textarea');
|
||||
const attachRow = document.getElementById('chat-attach-row');
|
||||
@ -3155,7 +3371,16 @@ const Stage1 = (function () {
|
||||
saveState();
|
||||
renderChat();
|
||||
setTimeout(() => {
|
||||
const directionIntent = /小红书|种草|测评|评测|痛点|熬夜|学生|宝妈|妈妈|人物|风格|口吻|换成/.test(v);
|
||||
if (mode && (!shots.length || directionIntent)) {
|
||||
tuneBriefByText(v);
|
||||
const intro = directionIntent
|
||||
? '已更新创作方向。确认后我会按这个方向重写镜头脚本。'
|
||||
: '我先根据你给的内容补全创作方向,确认后再生成镜头脚本。';
|
||||
pushMsg('ai', briefConfirmHtml(intro));
|
||||
} else {
|
||||
pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');
|
||||
}
|
||||
saveState();
|
||||
renderChat();
|
||||
}, 400);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user