// ==UserScript==
// @name 位导の自动分镜助手
// @namespace http://tampermonkey.net/
// @version 0.3
// @description 为创景平台添加自动分镜头功能,支持DeepSeek智能分镜
// @author Your name
// @match https://www.chanjing.cc/worktable*
// @grant GM_xmlhttpRequest
// @connect api.deepseek.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 更新样式
const style = document.createElement('style');
style.textContent = `
.director-entry {
position: fixed;
left: 50%;
transform: translateX(-50%);
top: 20px;
display: flex;
align-items: center;
gap: 8px;
background: #ffffff;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 9999;
}
.director-entry img {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover; /* 确保图片比例正确 */
}
.auto-shot-panel {
position: fixed;
right: 20px;
top: 20px;
background: #ffffff;
border-radius: 12px;
padding: 20px;
width: 800px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: none;
}
.shot-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.shot-table tr {
display: flex;
align-items: center;
margin-bottom: 10px;
width: 100%;
}
.shot-table td {
display: flex;
align-items: center;
width: 100%;
gap: 12px;
}
.shot-input {
width: 80px;
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
.text-input {
flex: 1;
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
.row-controls {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.row-btn {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #f0f0f0;
transition: background 0.2s;
}
.row-btn:hover {
background: #e0e0e0;
}
.action-btn {
width: 100%;
padding: 12px;
background: #FC885E;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: opacity 0.2s;
}
.action-btn:hover {
opacity: 0.9;
}
.auto-shot-step1 {
position: fixed;
right: 20px;
top: 20px;
background: #ffffff;
border-radius: 12px;
padding: 20px;
width: 800px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: none;
}
.script-input {
width: 100%;
height: 300px;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
margin-bottom: 15px;
resize: vertical;
font-family: inherit;
}
.shot-settings {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.shot-setting-group {
flex: 1;
}
.shot-setting-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.next-btn {
width: 100%;
padding: 12px;
background: #FC885E;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: opacity 0.2s;
}
.next-btn:hover {
opacity: 0.9;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
display: none;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 6px solid #f3f3f3;
border-top: 6px solid #FC885E;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.shot-preview-container {
margin-bottom: 20px;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.shot-preview-item {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 6px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.shot-preview-item.selected {
border-color: #FC885E;
background: rgba(252, 136, 94, 0.05);
}
.shot-preview-img {
width: 100%;
height: 80px;
object-fit: contain;
border-radius: 4px;
background: #f5f5f5;
}
.shot-preview-caption {
font-size: 12px;
color: #333;
text-align: center;
}
`;
document.head.appendChild(style);
// 获取本地存储的数据
function getStoredData() {
const stored = localStorage.getItem('autoShotData');
if (stored) {
return JSON.parse(stored);
}
return [
{ shot: 1, text: '大家好我是位毛,这是我的新呆毛,功能是自动添加分镜头脚本' },
{ shot: 2, text: '目前仅支持新建全新的数字人,不能打开老工程使用' },
{ shot: 3, text: '我也不想把功能搞得太完善,不然产品化后我的外挂失效了,我会很失落(bushi)' }
];
}
// 保存数据到本地存储
function saveData() {
const rows = Array.from(document.querySelectorAll('#shotTable tr')).map(row => ({
shot: row.querySelector('.shot-input').value,
text: row.querySelector('.text-input').value
}));
localStorage.setItem('autoShotData', JSON.stringify(rows));
}
// 获取下一个分镜号
function getNextShotNumber(currentShot) {
const nextShot = (parseInt(currentShot) % 15) + 1;
return nextShot;
}
// 修改行创建函数
function createRow(shotNum = '', text = '') {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<select class="shot-input">
${[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(n =>
`<option value="${n}" ${n === parseInt(shotNum) ? 'selected' : ''}>${n}</option>`
).join('')}
</select>
<input type="text" class="text-input" placeholder="请输入台词" value="${text.replace(/"/g, '"')}">
<div class="row-controls">
<button class="row-btn add-row">+</button>
<button class="row-btn remove-row">-</button>
</div>
</td>
`;
return tr;
}
// 创建入口按钮
const entry = document.createElement('div');
entry.className = 'director-entry';
entry.innerHTML = `
<img src="https://img.weimao.me/ipic/2025-03-21-GIF%20%E5%A4%B4%E5%83%8F%20600k.gif" alt="导演图标">
<span>导演台本输入</span>
`;
document.body.appendChild(entry);
// 重要:创建第二步界面(原始分镜界面)
const panel = document.createElement('div');
panel.className = 'auto-shot-panel';
panel.innerHTML = `
<table class="shot-table" id="shotTable">
<tbody></tbody>
</table>
<button class="action-btn" id="actionBtn">Action!</button>
`;
document.body.appendChild(panel);
// 初始化第二步界面中的表格内容
const tbody = panel.querySelector('#shotTable tbody');
getStoredData().forEach(row => {
tbody.appendChild(createRow(row.shot, row.text));
});
// 创建第一步界面
const step1Panel = document.createElement('div');
step1Panel.className = 'auto-shot-step1';
step1Panel.innerHTML = `
<h2 style="margin-top: 0; margin-bottom: 15px;">台本自动分镜</h2>
<textarea class="script-input" placeholder="请输入完整台本..."></textarea>
<div class="shot-settings">
<div class="shot-setting-group">
<label>主机位</label>
<select class="main-shot-select">
${[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(n => `<option value="${n}">${n}</option>`).join('')}
</select>
</div>
<div class="shot-setting-group">
<label>侧机位</label>
<select class="side-shot-select">
${[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(n => `<option value="${n}" ${n === 2 ? 'selected' : ''}>${n}</option>`).join('')}
</select>
</div>
<div class="shot-setting-group">
<label>特写机位</label>
<select class="closeup-shot-select">
${[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(n => `<option value="${n}" ${n === 3 ? 'selected' : ''}>${n}</option>`).join('')}
</select>
</div>
</div>
<button class="next-btn" id="autoShotBtn">智能分镜</button>
`;
document.body.appendChild(step1Panel);
// 创建loading遮罩
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'loading-overlay';
loadingOverlay.innerHTML = `<div class="loading-spinner"></div>`;
document.body.appendChild(loadingOverlay);
// 事件处理
document.addEventListener('click', async function(e) {
// 处理添加行
if (e.target.classList.contains('add-row')) {
const currentRow = e.target.closest('tr');
const currentShot = currentRow.querySelector('.shot-input').value;
const nextShot = getNextShotNumber(currentShot);
const newRow = createRow(nextShot, '');
currentRow.after(newRow);
saveData(); // 保存更新后的数据
}
// 处理删除行
if (e.target.classList.contains('remove-row')) {
const tbody = document.querySelector('#shotTable tbody');
if (tbody.children.length > 1) {
e.target.closest('tr').remove();
saveData(); // 保存更新后的数据
}
}
// Action按钮处理
if (e.target.id === 'actionBtn') {
// 保存当前数据
saveData();
// 隐藏面板
document.querySelector('.auto-shot-panel').style.display = 'none';
const rows = Array.from(document.querySelectorAll('#shotTable tr')).map(row => ({
shot: row.querySelector('.shot-input').value,
text: row.querySelector('.text-input').value
}));
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const isLastRow = i === rows.length - 1; // 判断是否是最后一行
// 选择对应的镜头
const shots = document.querySelectorAll('.custom-list.pack-up .custom-img.drag-child');
const targetShot = shots[row.shot - 1];
if (targetShot) {
targetShot.click();
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 500));
// 填入台词
const editor = document.querySelector('.com-script-editor .ProseMirror');
if (editor) {
editor.innerHTML = `<p>${row.text}</p>`;
const event = new Event('input', { bubbles: true });
editor.dispatchEvent(event);
}
// 收起时间轴
const unfoldBtn = document.querySelector('.unfold-label.unfold');
if (unfoldBtn) unfoldBtn.click();
await new Promise(resolve => setTimeout(resolve, 300));
// 只在不是最后一行时添加新镜头
if (!isLastRow) {
const addBtn = document.querySelector('.add-button');
if (addBtn) addBtn.click();
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
}
// 智能分镜按钮处理
if (e.target.id === 'autoShotBtn') {
const script = document.querySelector('.script-input').value.trim();
if (!script) {
alert('请输入台本内容');
return;
}
const mainShot = document.querySelector('.main-shot-select').value;
const sideShot = document.querySelector('.side-shot-select').value;
const closeupShot = document.querySelector('.closeup-shot-select').value;
const results = await callDeepSeekAPI(script, mainShot, sideShot, closeupShot);
if (results && results.length > 0) {
console.log('填充结果到第二步界面:', results);
fillStepTwoWithResults(results);
// 隐藏第一步,显示第二步
step1Panel.style.display = 'none';
panel.style.display = 'block';
} else {
alert('分镜结果为空,请重试');
}
}
});
// 监听输入变化,实时保存
document.addEventListener('input', function(e) {
if (e.target.classList.contains('shot-input') ||
e.target.classList.contains('text-input')) {
saveData();
}
});
// 修改入口按钮的点击事件,显示第一步界面
entry.addEventListener('click', function() {
step1Panel.style.display = 'block';
panel.style.display = 'none'; // 确保第二步界面隐藏
// 延迟获取镜头预览,确保DOM已加载
setTimeout(() => {
updateStepOneWithPreviews();
}, 500);
});
// 调用DeepSeek API进行自动分镜
async function callDeepSeekAPI(script, mainShot, sideShot, closeupShot) {
loadingOverlay.style.display = 'flex';
const prompt = `请将以下内容进行分句,并根据内容安排机位(主机位、侧机位、特写机位)。
1. 不要修改任何文本内容,只进行分句;
2. 你现在就是一个专业的短剧导演,请根据分句的表意、情绪、节奏选择合适的机位。
3. 主机位对应分镜号${mainShot},侧机位对应分镜号${sideShot},特写机位对应分镜号${closeupShot}。
4. 输出时格式严格按照:分镜号+空格+台词,每行一句。
台本内容:
${script}`;
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-d4102372de644218bc71c6c59ddcdeb7'
},
body: JSON.stringify({
model: 'deepseek-chat',
messages: [
{
role: 'user',
content: prompt
}
],
temperature: 0.7
})
});
const data = await response.json();
console.log('DeepSeek API 响应:', data);
if (data.choices && data.choices.length > 0) {
const parsedResults = parseDeepSeekResponse(data.choices[0].message.content);
console.log('解析结果:', parsedResults);
return parsedResults;
} else {
console.error('DeepSeek API 返回异常:', data);
throw new Error('获取DeepSeek响应失败');
}
} catch (error) {
console.error('调用DeepSeek API出错:', error);
alert('自动分镜失败,请检查网络或重试: ' + error.message);
return null;
} finally {
loadingOverlay.style.display = 'none';
}
}
// 解析DeepSeek响应
function parseDeepSeekResponse(content) {
console.log('解析原始响应:', content);
const lines = content.split('\n').filter(line => line.trim());
const result = [];
for (const line of lines) {
// 尝试匹配 "数字 文本" 的格式
const match = line.match(/^(\d+)\s+(.+)$/);
if (match) {
result.push({
shot: match[1],
text: match[2]
});
}
}
return result;
}
// 用解析的结果填充第二步界面
function fillStepTwoWithResults(results) {
const tbody = document.querySelector('#shotTable tbody');
if (!tbody) {
console.error('未找到表格主体元素');
return;
}
// 清空现有内容
tbody.innerHTML = '';
// 填充新内容
for (const row of results) {
const tr = createRow(row.shot, row.text);
tbody.appendChild(tr);
}
// 保存到本地存储
saveData();
}
// 获取镜头缩略图
function getShotPreviews() {
const shotImages = document.querySelectorAll('.custom-list.pack-up .custom-img.drag-child');
const previews = [];
shotImages.forEach((img, index) => {
if (index < 15) { // 只取前15个
const imgSrc = img.src || img.querySelector('img')?.src || '';
previews.push({
index: index + 1,
src: imgSrc
});
}
});
return previews;
}
// 创建缩略图HTML
function createPreviewsHTML(previews) {
if (!previews || previews.length === 0) {
return '<div class="shot-preview-container"><p>未找到可用的镜头预览</p></div>';
}
let html = '<div class="shot-preview-container">';
previews.forEach(preview => {
html += `
<div class="shot-preview-item" data-shot="${preview.index}">
<img src="${preview.src}" class="shot-preview-img" alt="镜头 ${preview.index}">
<div class="shot-preview-caption">镜头 ${preview.index}</div>
</div>
`;
});
html += '</div>';
return html;
}
// 更新第一步界面,添加机位预览
function updateStepOneWithPreviews() {
const shotSettingsContainer = document.querySelector('.shot-settings');
const previewContainer = document.querySelector('.shot-preview-container');
if (previewContainer) {
previewContainer.remove();
}
const previews = getShotPreviews();
const previewsHTML = createPreviewsHTML(previews);
shotSettingsContainer.insertAdjacentHTML('beforebegin', previewsHTML);
// 添加选中效果
updatePreviewSelection();
}
// 更新缩略图选中状态
function updatePreviewSelection() {
const mainShot = document.querySelector('.main-shot-select').value;
const sideShot = document.querySelector('.side-shot-select').value;
const closeupShot = document.querySelector('.closeup-shot-select').value;
document.querySelectorAll('.shot-preview-item').forEach(item => {
item.classList.remove('selected');
const shotIndex = item.getAttribute('data-shot');
if (shotIndex === mainShot) {
item.classList.add('selected');
item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex} (主机位)`;
} else if (shotIndex === sideShot) {
item.classList.add('selected');
item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex} (侧机位)`;
} else if (shotIndex === closeupShot) {
item.classList.add('selected');
item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex} (特写机位)`;
} else {
item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex}`;
}
});
}
// 监听机位选择变化
document.addEventListener('change', function(e) {
if (e.target.classList.contains('main-shot-select') ||
e.target.classList.contains('side-shot-select') ||
e.target.classList.contains('closeup-shot-select')) {
updatePreviewSelection();
}
});
// 添加缩略图点击事件
document.addEventListener('click', function(e) {
const previewItem = e.target.closest('.shot-preview-item');
if (previewItem) {
const shotIndex = previewItem.getAttribute('data-shot');
// 如果用户点击了预览图,询问设置为哪种机位
const options = ["主机位", "侧机位", "特写机位"];
const selected = window.prompt(`将镜头 ${shotIndex} 设置为:`, "主机位");
if (selected) {
if (selected.includes("主")) {
document.querySelector('.main-shot-select').value = shotIndex;
} else if (selected.includes("侧")) {
document.querySelector('.side-shot-select').value = shotIndex;
} else if (selected.includes("特")) {
document.querySelector('.closeup-shot-select').value = shotIndex;
}
updatePreviewSelection();
}
}
});
})();