- // ==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();
- }
- }
- });
- })();