Greasy Fork镜像 支持简体中文。

位导の自动分镜助手

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

  1. // ==UserScript==
  2. // @name 位导の自动分镜助手
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description 为创景平台添加自动分镜头功能,支持DeepSeek智能分镜
  6. // @author Your name
  7. // @match https://www.chanjing.cc/worktable*
  8. // @grant GM_xmlhttpRequest
  9. // @connect api.deepseek.com
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // 更新样式
  17. const style = document.createElement('style');
  18. style.textContent = `
  19. .director-entry {
  20. position: fixed;
  21. left: 50%;
  22. transform: translateX(-50%);
  23. top: 20px;
  24. display: flex;
  25. align-items: center;
  26. gap: 8px;
  27. background: #ffffff;
  28. padding: 8px 16px;
  29. border-radius: 8px;
  30. cursor: pointer;
  31. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  32. z-index: 9999;
  33. }
  34.  
  35. .director-entry img {
  36. width: 40px;
  37. height: 40px;
  38. border-radius: 4px;
  39. object-fit: cover; /* 确保图片比例正确 */
  40. }
  41.  
  42. .auto-shot-panel {
  43. position: fixed;
  44. right: 20px;
  45. top: 20px;
  46. background: #ffffff;
  47. border-radius: 12px;
  48. padding: 20px;
  49. width: 800px;
  50. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  51. z-index: 9999;
  52. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  53. display: none;
  54. }
  55.  
  56. .shot-table {
  57. width: 100%;
  58. border-collapse: collapse;
  59. margin-bottom: 15px;
  60. }
  61.  
  62. .shot-table tr {
  63. display: flex;
  64. align-items: center;
  65. margin-bottom: 10px;
  66. width: 100%;
  67. }
  68.  
  69. .shot-table td {
  70. display: flex;
  71. align-items: center;
  72. width: 100%;
  73. gap: 12px;
  74. }
  75.  
  76. .shot-input {
  77. width: 80px;
  78. padding: 8px;
  79. border: 1px solid #e0e0e0;
  80. border-radius: 6px;
  81. }
  82.  
  83. .text-input {
  84. flex: 1;
  85. padding: 8px;
  86. border: 1px solid #e0e0e0;
  87. border-radius: 6px;
  88. }
  89.  
  90. .row-controls {
  91. display: flex;
  92. gap: 4px;
  93. flex-shrink: 0;
  94. }
  95.  
  96. .row-btn {
  97. padding: 4px 12px;
  98. border: none;
  99. border-radius: 4px;
  100. cursor: pointer;
  101. background: #f0f0f0;
  102. transition: background 0.2s;
  103. }
  104.  
  105. .row-btn:hover {
  106. background: #e0e0e0;
  107. }
  108.  
  109. .action-btn {
  110. width: 100%;
  111. padding: 12px;
  112. background: #FC885E;
  113. color: white;
  114. border: none;
  115. border-radius: 6px;
  116. cursor: pointer;
  117. font-weight: 500;
  118. transition: opacity 0.2s;
  119. }
  120.  
  121. .action-btn:hover {
  122. opacity: 0.9;
  123. }
  124.  
  125. .auto-shot-step1 {
  126. position: fixed;
  127. right: 20px;
  128. top: 20px;
  129. background: #ffffff;
  130. border-radius: 12px;
  131. padding: 20px;
  132. width: 800px;
  133. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  134. z-index: 9999;
  135. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  136. display: none;
  137. }
  138.  
  139. .script-input {
  140. width: 100%;
  141. height: 300px;
  142. padding: 12px;
  143. border: 1px solid #e0e0e0;
  144. border-radius: 6px;
  145. margin-bottom: 15px;
  146. resize: vertical;
  147. font-family: inherit;
  148. }
  149.  
  150. .shot-settings {
  151. display: flex;
  152. gap: 20px;
  153. margin-bottom: 15px;
  154. }
  155.  
  156. .shot-setting-group {
  157. flex: 1;
  158. }
  159.  
  160. .shot-setting-group label {
  161. display: block;
  162. margin-bottom: 8px;
  163. font-weight: 500;
  164. }
  165.  
  166. .next-btn {
  167. width: 100%;
  168. padding: 12px;
  169. background: #FC885E;
  170. color: white;
  171. border: none;
  172. border-radius: 6px;
  173. cursor: pointer;
  174. font-weight: 500;
  175. transition: opacity 0.2s;
  176. }
  177.  
  178. .next-btn:hover {
  179. opacity: 0.9;
  180. }
  181.  
  182. .loading-overlay {
  183. position: fixed;
  184. top: 0;
  185. left: 0;
  186. width: 100%;
  187. height: 100%;
  188. background: rgba(0, 0, 0, 0.5);
  189. display: flex;
  190. justify-content: center;
  191. align-items: center;
  192. z-index: 10000;
  193. display: none;
  194. }
  195.  
  196. .loading-spinner {
  197. width: 60px;
  198. height: 60px;
  199. border: 6px solid #f3f3f3;
  200. border-top: 6px solid #FC885E;
  201. border-radius: 50%;
  202. animation: spin 1s linear infinite;
  203. }
  204.  
  205. @keyframes spin {
  206. 0% { transform: rotate(0deg); }
  207. 100% { transform: rotate(360deg); }
  208. }
  209.  
  210. .shot-preview-container {
  211. margin-bottom: 20px;
  212. display: grid;
  213. grid-template-columns: repeat(5, 1fr);
  214. gap: 8px;
  215. }
  216.  
  217. .shot-preview-item {
  218. border: 1px solid #e0e0e0;
  219. border-radius: 6px;
  220. padding: 6px;
  221. display: flex;
  222. flex-direction: column;
  223. align-items: center;
  224. gap: 6px;
  225. }
  226.  
  227. .shot-preview-item.selected {
  228. border-color: #FC885E;
  229. background: rgba(252, 136, 94, 0.05);
  230. }
  231.  
  232. .shot-preview-img {
  233. width: 100%;
  234. height: 80px;
  235. object-fit: contain;
  236. border-radius: 4px;
  237. background: #f5f5f5;
  238. }
  239.  
  240. .shot-preview-caption {
  241. font-size: 12px;
  242. color: #333;
  243. text-align: center;
  244. }
  245. `;
  246. document.head.appendChild(style);
  247.  
  248. // 获取本地存储的数据
  249. function getStoredData() {
  250. const stored = localStorage.getItem('autoShotData');
  251. if (stored) {
  252. return JSON.parse(stored);
  253. }
  254. return [
  255. { shot: 1, text: '大家好我是位毛,这是我的新呆毛,功能是自动添加分镜头脚本' },
  256. { shot: 2, text: '目前仅支持新建全新的数字人,不能打开老工程使用' },
  257. { shot: 3, text: '我也不想把功能搞得太完善,不然产品化后我的外挂失效了,我会很失落(bushi)' }
  258. ];
  259. }
  260.  
  261. // 保存数据到本地存储
  262. function saveData() {
  263. const rows = Array.from(document.querySelectorAll('#shotTable tr')).map(row => ({
  264. shot: row.querySelector('.shot-input').value,
  265. text: row.querySelector('.text-input').value
  266. }));
  267. localStorage.setItem('autoShotData', JSON.stringify(rows));
  268. }
  269.  
  270. // 获取下一个分镜号
  271. function getNextShotNumber(currentShot) {
  272. const nextShot = (parseInt(currentShot) % 15) + 1;
  273. return nextShot;
  274. }
  275.  
  276. // 修改行创建函数
  277. function createRow(shotNum = '', text = '') {
  278. const tr = document.createElement('tr');
  279. tr.innerHTML = `
  280. <td>
  281. <select class="shot-input">
  282. ${[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(n =>
  283. `<option value="${n}" ${n === parseInt(shotNum) ? 'selected' : ''}>${n}</option>`
  284. ).join('')}
  285. </select>
  286. <input type="text" class="text-input" placeholder="请输入台词" value="${text.replace(/"/g, '&quot;')}">
  287. <div class="row-controls">
  288. <button class="row-btn add-row">+</button>
  289. <button class="row-btn remove-row">-</button>
  290. </div>
  291. </td>
  292. `;
  293. return tr;
  294. }
  295.  
  296. // 创建入口按钮
  297. const entry = document.createElement('div');
  298. entry.className = 'director-entry';
  299. entry.innerHTML = `
  300. <img src="https://img.weimao.me/ipic/2025-03-21-GIF%20%E5%A4%B4%E5%83%8F%20600k.gif" alt="导演图标">
  301. <span>导演台本输入</span>
  302. `;
  303. document.body.appendChild(entry);
  304.  
  305. // 重要:创建第二步界面(原始分镜界面)
  306. const panel = document.createElement('div');
  307. panel.className = 'auto-shot-panel';
  308. panel.innerHTML = `
  309. <table class="shot-table" id="shotTable">
  310. <tbody></tbody>
  311. </table>
  312. <button class="action-btn" id="actionBtn">Action!</button>
  313. `;
  314. document.body.appendChild(panel);
  315.  
  316. // 初始化第二步界面中的表格内容
  317. const tbody = panel.querySelector('#shotTable tbody');
  318. getStoredData().forEach(row => {
  319. tbody.appendChild(createRow(row.shot, row.text));
  320. });
  321.  
  322. // 创建第一步界面
  323. const step1Panel = document.createElement('div');
  324. step1Panel.className = 'auto-shot-step1';
  325. step1Panel.innerHTML = `
  326. <h2 style="margin-top: 0; margin-bottom: 15px;">台本自动分镜</h2>
  327. <textarea class="script-input" placeholder="请输入完整台本..."></textarea>
  328. <div class="shot-settings">
  329. <div class="shot-setting-group">
  330. <label>主机位</label>
  331. <select class="main-shot-select">
  332. ${[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(n => `<option value="${n}">${n}</option>`).join('')}
  333. </select>
  334. </div>
  335. <div class="shot-setting-group">
  336. <label>侧机位</label>
  337. <select class="side-shot-select">
  338. ${[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('')}
  339. </select>
  340. </div>
  341. <div class="shot-setting-group">
  342. <label>特写机位</label>
  343. <select class="closeup-shot-select">
  344. ${[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('')}
  345. </select>
  346. </div>
  347. </div>
  348. <button class="next-btn" id="autoShotBtn">智能分镜</button>
  349. `;
  350. document.body.appendChild(step1Panel);
  351.  
  352. // 创建loading遮罩
  353. const loadingOverlay = document.createElement('div');
  354. loadingOverlay.className = 'loading-overlay';
  355. loadingOverlay.innerHTML = `<div class="loading-spinner"></div>`;
  356. document.body.appendChild(loadingOverlay);
  357.  
  358. // 事件处理
  359. document.addEventListener('click', async function(e) {
  360. // 处理添加行
  361. if (e.target.classList.contains('add-row')) {
  362. const currentRow = e.target.closest('tr');
  363. const currentShot = currentRow.querySelector('.shot-input').value;
  364. const nextShot = getNextShotNumber(currentShot);
  365. const newRow = createRow(nextShot, '');
  366. currentRow.after(newRow);
  367. saveData(); // 保存更新后的数据
  368. }
  369.  
  370. // 处理删除行
  371. if (e.target.classList.contains('remove-row')) {
  372. const tbody = document.querySelector('#shotTable tbody');
  373. if (tbody.children.length > 1) {
  374. e.target.closest('tr').remove();
  375. saveData(); // 保存更新后的数据
  376. }
  377. }
  378.  
  379. // Action按钮处理
  380. if (e.target.id === 'actionBtn') {
  381. // 保存当前数据
  382. saveData();
  383.  
  384. // 隐藏面板
  385. document.querySelector('.auto-shot-panel').style.display = 'none';
  386.  
  387. const rows = Array.from(document.querySelectorAll('#shotTable tr')).map(row => ({
  388. shot: row.querySelector('.shot-input').value,
  389. text: row.querySelector('.text-input').value
  390. }));
  391.  
  392. for (let i = 0; i < rows.length; i++) {
  393. const row = rows[i];
  394. const isLastRow = i === rows.length - 1; // 判断是否是最后一行
  395.  
  396. // 选择对应的镜头
  397. const shots = document.querySelectorAll('.custom-list.pack-up .custom-img.drag-child');
  398. const targetShot = shots[row.shot - 1];
  399. if (targetShot) {
  400. targetShot.click();
  401.  
  402. // 等待编辑器加载
  403. await new Promise(resolve => setTimeout(resolve, 500));
  404.  
  405. // 填入台词
  406. const editor = document.querySelector('.com-script-editor .ProseMirror');
  407. if (editor) {
  408. editor.innerHTML = `<p>${row.text}</p>`;
  409. const event = new Event('input', { bubbles: true });
  410. editor.dispatchEvent(event);
  411. }
  412.  
  413. // 收起时间轴
  414. const unfoldBtn = document.querySelector('.unfold-label.unfold');
  415. if (unfoldBtn) unfoldBtn.click();
  416.  
  417. await new Promise(resolve => setTimeout(resolve, 300));
  418.  
  419. // 只在不是最后一行时添加新镜头
  420. if (!isLastRow) {
  421. const addBtn = document.querySelector('.add-button');
  422. if (addBtn) addBtn.click();
  423. await new Promise(resolve => setTimeout(resolve, 500));
  424. }
  425. }
  426. }
  427. }
  428.  
  429. // 智能分镜按钮处理
  430. if (e.target.id === 'autoShotBtn') {
  431. const script = document.querySelector('.script-input').value.trim();
  432. if (!script) {
  433. alert('请输入台本内容');
  434. return;
  435. }
  436.  
  437. const mainShot = document.querySelector('.main-shot-select').value;
  438. const sideShot = document.querySelector('.side-shot-select').value;
  439. const closeupShot = document.querySelector('.closeup-shot-select').value;
  440.  
  441. const results = await callDeepSeekAPI(script, mainShot, sideShot, closeupShot);
  442.  
  443. if (results && results.length > 0) {
  444. console.log('填充结果到第二步界面:', results);
  445. fillStepTwoWithResults(results);
  446.  
  447. // 隐藏第一步,显示第二步
  448. step1Panel.style.display = 'none';
  449. panel.style.display = 'block';
  450. } else {
  451. alert('分镜结果为空,请重试');
  452. }
  453. }
  454. });
  455.  
  456. // 监听输入变化,实时保存
  457. document.addEventListener('input', function(e) {
  458. if (e.target.classList.contains('shot-input') ||
  459. e.target.classList.contains('text-input')) {
  460. saveData();
  461. }
  462. });
  463.  
  464. // 修改入口按钮的点击事件,显示第一步界面
  465. entry.addEventListener('click', function() {
  466. step1Panel.style.display = 'block';
  467. panel.style.display = 'none'; // 确保第二步界面隐藏
  468. // 延迟获取镜头预览,确保DOM已加载
  469. setTimeout(() => {
  470. updateStepOneWithPreviews();
  471. }, 500);
  472. });
  473.  
  474. // 调用DeepSeek API进行自动分镜
  475. async function callDeepSeekAPI(script, mainShot, sideShot, closeupShot) {
  476. loadingOverlay.style.display = 'flex';
  477.  
  478. const prompt = `请将以下内容进行分句,并根据内容安排机位(主机位、侧机位、特写机位)。
  479. 1. 不要修改任何文本内容,只进行分句;
  480. 2. 你现在就是一个专业的短剧导演,请根据分句的表意、情绪、节奏选择合适的机位。
  481. 3. 主机位对应分镜号${mainShot},侧机位对应分镜号${sideShot},特写机位对应分镜号${closeupShot}。
  482. 4. 输出时格式严格按照:分镜号+空格+台词,每行一句。
  483.  
  484. 台本内容:
  485. ${script}`;
  486.  
  487. try {
  488. const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
  489. method: 'POST',
  490. headers: {
  491. 'Content-Type': 'application/json',
  492. 'Authorization': 'Bearer sk-d4102372de644218bc71c6c59ddcdeb7'
  493. },
  494. body: JSON.stringify({
  495. model: 'deepseek-chat',
  496. messages: [
  497. {
  498. role: 'user',
  499. content: prompt
  500. }
  501. ],
  502. temperature: 0.7
  503. })
  504. });
  505.  
  506. const data = await response.json();
  507. console.log('DeepSeek API 响应:', data);
  508.  
  509. if (data.choices && data.choices.length > 0) {
  510. const parsedResults = parseDeepSeekResponse(data.choices[0].message.content);
  511. console.log('解析结果:', parsedResults);
  512. return parsedResults;
  513. } else {
  514. console.error('DeepSeek API 返回异常:', data);
  515. throw new Error('获取DeepSeek响应失败');
  516. }
  517. } catch (error) {
  518. console.error('调用DeepSeek API出错:', error);
  519. alert('自动分镜失败,请检查网络或重试: ' + error.message);
  520. return null;
  521. } finally {
  522. loadingOverlay.style.display = 'none';
  523. }
  524. }
  525.  
  526. // 解析DeepSeek响应
  527. function parseDeepSeekResponse(content) {
  528. console.log('解析原始响应:', content);
  529.  
  530. const lines = content.split('\n').filter(line => line.trim());
  531. const result = [];
  532.  
  533. for (const line of lines) {
  534. // 尝试匹配 "数字 文本" 的格式
  535. const match = line.match(/^(\d+)\s+(.+)$/);
  536. if (match) {
  537. result.push({
  538. shot: match[1],
  539. text: match[2]
  540. });
  541. }
  542. }
  543.  
  544. return result;
  545. }
  546.  
  547. // 用解析的结果填充第二步界面
  548. function fillStepTwoWithResults(results) {
  549. const tbody = document.querySelector('#shotTable tbody');
  550. if (!tbody) {
  551. console.error('未找到表格主体元素');
  552. return;
  553. }
  554.  
  555. // 清空现有内容
  556. tbody.innerHTML = '';
  557.  
  558. // 填充新内容
  559. for (const row of results) {
  560. const tr = createRow(row.shot, row.text);
  561. tbody.appendChild(tr);
  562. }
  563.  
  564. // 保存到本地存储
  565. saveData();
  566. }
  567.  
  568. // 获取镜头缩略图
  569. function getShotPreviews() {
  570. const shotImages = document.querySelectorAll('.custom-list.pack-up .custom-img.drag-child');
  571. const previews = [];
  572.  
  573. shotImages.forEach((img, index) => {
  574. if (index < 15) { // 只取前15个
  575. const imgSrc = img.src || img.querySelector('img')?.src || '';
  576. previews.push({
  577. index: index + 1,
  578. src: imgSrc
  579. });
  580. }
  581. });
  582.  
  583. return previews;
  584. }
  585.  
  586. // 创建缩略图HTML
  587. function createPreviewsHTML(previews) {
  588. if (!previews || previews.length === 0) {
  589. return '<div class="shot-preview-container"><p>未找到可用的镜头预览</p></div>';
  590. }
  591.  
  592. let html = '<div class="shot-preview-container">';
  593. previews.forEach(preview => {
  594. html += `
  595. <div class="shot-preview-item" data-shot="${preview.index}">
  596. <img src="${preview.src}" class="shot-preview-img" alt="镜头 ${preview.index}">
  597. <div class="shot-preview-caption">镜头 ${preview.index}</div>
  598. </div>
  599. `;
  600. });
  601. html += '</div>';
  602.  
  603. return html;
  604. }
  605.  
  606. // 更新第一步界面,添加机位预览
  607. function updateStepOneWithPreviews() {
  608. const shotSettingsContainer = document.querySelector('.shot-settings');
  609. const previewContainer = document.querySelector('.shot-preview-container');
  610.  
  611. if (previewContainer) {
  612. previewContainer.remove();
  613. }
  614.  
  615. const previews = getShotPreviews();
  616. const previewsHTML = createPreviewsHTML(previews);
  617.  
  618. shotSettingsContainer.insertAdjacentHTML('beforebegin', previewsHTML);
  619.  
  620. // 添加选中效果
  621. updatePreviewSelection();
  622. }
  623.  
  624. // 更新缩略图选中状态
  625. function updatePreviewSelection() {
  626. const mainShot = document.querySelector('.main-shot-select').value;
  627. const sideShot = document.querySelector('.side-shot-select').value;
  628. const closeupShot = document.querySelector('.closeup-shot-select').value;
  629.  
  630. document.querySelectorAll('.shot-preview-item').forEach(item => {
  631. item.classList.remove('selected');
  632. const shotIndex = item.getAttribute('data-shot');
  633.  
  634. if (shotIndex === mainShot) {
  635. item.classList.add('selected');
  636. item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex} (主机位)`;
  637. } else if (shotIndex === sideShot) {
  638. item.classList.add('selected');
  639. item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex} (侧机位)`;
  640. } else if (shotIndex === closeupShot) {
  641. item.classList.add('selected');
  642. item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex} (特写机位)`;
  643. } else {
  644. item.querySelector('.shot-preview-caption').textContent = `镜头 ${shotIndex}`;
  645. }
  646. });
  647. }
  648.  
  649. // 监听机位选择变化
  650. document.addEventListener('change', function(e) {
  651. if (e.target.classList.contains('main-shot-select') ||
  652. e.target.classList.contains('side-shot-select') ||
  653. e.target.classList.contains('closeup-shot-select')) {
  654. updatePreviewSelection();
  655. }
  656. });
  657.  
  658. // 添加缩略图点击事件
  659. document.addEventListener('click', function(e) {
  660. const previewItem = e.target.closest('.shot-preview-item');
  661. if (previewItem) {
  662. const shotIndex = previewItem.getAttribute('data-shot');
  663.  
  664. // 如果用户点击了预览图,询问设置为哪种机位
  665. const options = ["主机位", "侧机位", "特写机位"];
  666. const selected = window.prompt(`将镜头 ${shotIndex} 设置为:`, "主机位");
  667.  
  668. if (selected) {
  669. if (selected.includes("主")) {
  670. document.querySelector('.main-shot-select').value = shotIndex;
  671. } else if (selected.includes("侧")) {
  672. document.querySelector('.side-shot-select').value = shotIndex;
  673. } else if (selected.includes("特")) {
  674. document.querySelector('.closeup-shot-select').value = shotIndex;
  675. }
  676.  
  677. updatePreviewSelection();
  678. }
  679. }
  680. });
  681. })();

QingJ © 2025

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