位导の自动分镜助手

为创景平台添加自动分镜头功能,支持DeepSeek智能分镜

当前为 2025-04-11 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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, '&quot;')}">
                <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();
            }
        }
    });
})();