位导の自动分镜助手

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址