cleanup: rm user.js, untrack copilot_proxy.pyw; ljqCtrl: DPI via GDI + GrabWindow(hwnd)

This commit is contained in:
Liang Jiaqing
2026-04-10 22:17:24 +08:00
parent 5a1d3a41da
commit 5e28902cb4
4 changed files with 17 additions and 626 deletions

1
.gitignore vendored
View File

@@ -75,6 +75,7 @@ restore_commit.txt
sche_tasks/
# CDP Bridge 密钥配置(首次运行自动生成)
assets/tmwd_cdp_bridge/config.js
assets/copilot_proxy.pyw
**log.*
# Reflect (ignore new files, whitelist existing)

View File

@@ -1,185 +0,0 @@
"""
Copilot Local Proxy - TK GUI
本地 OpenAI 兼容代理,自动管理 Copilot token 并转发请求
"""
import tkinter as tk
from tkinter import scrolledtext
import threading, json, os, time, uuid
from http.server import HTTPServer, BaseHTTPRequestHandler
import requests
# ============ Config ============
OAUTH_PATH = os.path.join(os.path.expanduser('~'), '.copilot_oauth.json')
COPILOT_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token'
COPILOT_API_BASE = 'https://api.githubcopilot.com'
PROXY = {'https': 'http://127.0.0.1:2082'}
LOCAL_PORT = 15432
REFRESH_MARGIN = 120 # 提前120秒刷新
COPILOT_HEADERS = {
'Editor-Version': 'vscode/1.110.1',
'Editor-Plugin-Version': 'copilot-chat/0.38.2',
'User-Agent': 'GitHubCopilotChat/0.38.2',
'Copilot-Integration-Id': 'vscode-chat',
'openai-intent': 'conversation-panel',
}
# ============ Token Manager ============
class TokenManager:
def __init__(self, log_fn=print):
self.copilot_token = None
self.expires_at = 0
self.log = log_fn
self._lock = threading.Lock()
with open(OAUTH_PATH) as f:
self.access_token = json.load(f)['access_token']
self.log(f"[Token] OAuth token loaded: ***{self.access_token[-6:]}")
def get_token(self):
with self._lock:
if time.time() < self.expires_at - REFRESH_MARGIN:
return self.copilot_token
return self._refresh()
def _refresh(self):
self.log("[Token] Refreshing copilot token...")
try:
resp = requests.get(COPILOT_TOKEN_URL, headers={
'Authorization': f'token {self.access_token}',
'User-Agent': 'GitHubCopilotChat/0.38.2',
'Accept': 'application/json',
}, proxies=PROXY, timeout=15)
resp.raise_for_status()
data = resp.json()
self.copilot_token = data['token']
self.expires_at = data['expires_at']
remain = int(self.expires_at - time.time())
self.log(f"[Token] Refreshed OK, expires in {remain}s")
return self.copilot_token
except Exception as e:
self.log(f"[Token] Refresh FAILED: {e}")
return self.copilot_token
# ============ Proxy Handler ============
class ProxyHandler(BaseHTTPRequestHandler):
token_mgr: TokenManager = None
log_fn = print
def do_POST(self):
try:
length = int(self.headers.get('Content-Length', 0))
body = json.loads(self.rfile.read(length)) if length else {}
model = body.get('model', '?')
stream = body.get('stream', False)
self.log_fn(f"[Req] {model} stream={stream}")
token = self.token_mgr.get_token()
if not token:
self._error(503, "No copilot token available")
return
headers = {**COPILOT_HEADERS,
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
'x-request-id': str(uuid.uuid4())}
path = self.path
if path.startswith('/v1/'):
path = path[3:] # strip /v1 prefix
target = f"{COPILOT_API_BASE}{path}"
resp = requests.post(target, headers=headers, json=body,
proxies=PROXY, timeout=120, stream=stream)
if stream and 'text/event-stream' in resp.headers.get('content-type', ''):
self.send_response(resp.status_code)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Cache-Control', 'no-cache')
self.end_headers()
for chunk in resp.iter_content(chunk_size=None):
if chunk:
self.wfile.write(chunk)
self.wfile.flush()
else:
self.send_response(resp.status_code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(resp.content)
self.log_fn(f"[Resp] {resp.status_code}")
except Exception as e:
self.log_fn(f"[Error] {e}")
self._error(502, str(e))
def do_GET(self):
try:
token = self.token_mgr.get_token()
headers = {**COPILOT_HEADERS, 'Authorization': f'Bearer {token}'}
path = self.path
if path.startswith('/v1/'):
path = path[3:]
target = f"{COPILOT_API_BASE}{path}"
resp = requests.get(target, headers=headers, proxies=PROXY, timeout=15)
self.send_response(resp.status_code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(resp.content)
except Exception as e:
self._error(502, str(e))
def _error(self, code, msg):
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'error': msg}).encode())
def log_message(self, fmt, *args):
pass
# ============ TK GUI ============
class App:
def __init__(self):
self.root = tk.Tk()
self.root.title("Copilot Proxy")
self.root.geometry("520x360")
self.root.resizable(False, False)
frm = tk.Frame(self.root)
frm.pack(fill='x', padx=8, pady=4)
self.status_var = tk.StringVar(value="Starting...")
tk.Label(frm, textvariable=self.status_var, fg='blue', anchor='w').pack(side='left')
tk.Label(frm, text=f":{LOCAL_PORT}", fg='gray').pack(side='right')
self.log_area = scrolledtext.ScrolledText(
self.root, height=18, state='disabled', font=('Consolas', 9))
self.log_area.pack(fill='both', expand=True, padx=8, pady=4)
self.token_mgr = TokenManager(log_fn=self.log)
threading.Thread(target=self._run_server, daemon=True).start()
def log(self, msg):
ts = time.strftime('%H:%M:%S')
def _append():
self.log_area.config(state='normal')
self.log_area.insert('end', f"[{ts}] {msg}\n")
self.log_area.see('end')
self.log_area.config(state='disabled')
self.root.after(0, _append)
def _run_server(self):
ProxyHandler.token_mgr = self.token_mgr
ProxyHandler.log_fn = self.log
server = HTTPServer(('127.0.0.1', LOCAL_PORT), ProxyHandler)
self.log(f"[Server] Listening on http://127.0.0.1:{LOCAL_PORT}")
self.root.after(0, lambda: self.status_var.set(f"Running 127.0.0.1:{LOCAL_PORT}"))
self.token_mgr.get_token()
server.serve_forever()
def run(self):
self.root.mainloop()
if __name__ == '__main__':
App().run()

View File

@@ -1,422 +0,0 @@
// ==UserScript==
// @name ljq_web_driver
// @namespace http://tampermonkey.net/
// @version 0.40
// @description Execute JS via ljq_web_driver
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @author You
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant unsafeWindow
// @connect 127.0.0.1
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const log_prefix = "ljq_driver: ";
if (document.querySelector('[data-testid="stApp"],.stApp')) return;
if (/<title>\s*Streamlit\s*<\/title>|window\.prerenderReady=!1|You need to enable JavaScript to run this app\./i.test(document.documentElement?.outerHTML || '')) return;
if (window.self !== window.top) {
window.addEventListener('message',e=>{if(e.data?.type==='ljq_exec'){try{let r=eval(e.data.code);parent.postMessage({type:'ljq_result',id:e.data.id,result:String(r)},'*')}catch(err){parent.postMessage({type:'ljq_result',id:e.data.id,error:err.message},'*')}}});
return;
}
const wsUrl = 'ws://127.0.0.1:18765';
const httpUrl = 'http://127.0.0.1:18766/';
function isWebSocketServerAlive(callback) {
GM_xmlhttpRequest({
method: 'GET',
url: 'http://127.0.0.1:18765/',
onload: () => callback(true),
onerror: () => callback(false)
});
}
let ws;
let sid;
if (window.opener && window.name && window.name.startsWith('ljq_')) {
sid = null;
console.log(log_prefix + `检测到opener丢弃继承的window.name: ${window.name}`);
window.name = '';
} else {
sid = (window.name && window.name.startsWith('ljq_')) ? window.name : null;
}
if (!sid) {
sid = `ljq_${Date.now().toString().slice(-2)}${Math.random().toString(36).slice(2, 4)}`;
window.name = sid;
console.log(log_prefix + `创建新会话ID: ${sid}`);
} else {
console.log(log_prefix + `使用现有会话ID: ${sid}`);
}
// 保存会话ID
GM_setValue('sid', sid);
// 获取或创建状态指示器
function getIndicator() {
// 检查现有指示器
let ind = document.getElementById('ljq-ind');
// 删除重复指示器
const dups = document.querySelectorAll('[id="ljq-ind"]');
if (dups.length > 1) {
for (let i = 1; i < dups.length; i++) {
dups[i].remove();
}
ind = dups[0];
}
// 创建新指示器
if (!ind && document.body) {
ind = document.createElement('div');
ind.id = 'ljq-ind';
ind.style.cssText = `
position: fixed;bottom: 10px;
right: 10px;background-color: #f44336;
color: white;padding: 8px 12px;
border-radius: 6px;font-size: 14px;
font-weight: bold;z-index: 9999;
transition: background-color 0.3s;
cursor: pointer;box-shadow: 0 3px 6px rgba(0,0,0,0.25);
`;
ind.innerText = log_prefix + '正在连接...';
ind.addEventListener('click', () => alert(`会话ID: ${sid}\n当前URL: ${location.href}`));
document.body.appendChild(ind);
}
return ind;
}
// 更新状态
function updateStatus(status, msg) {
if (!document.body) return setTimeout(() => updateStatus(status, msg), 100);
const ind = getIndicator();
if (!ind) return;
if (status === 'ok') {
ind.style.backgroundColor = '#4CAF50';
ind.innerText = log_prefix + '连接成功';
} else if (status === 'disc') {
ind.style.backgroundColor = '#f44336';
ind.innerText = log_prefix + '连接断开';
} else if (status === 'conn') {
ind.style.backgroundColor = '#2196F3';
ind.innerText = log_prefix + '正在连接(HTTP)';
} else if (status === 'err') {
ind.style.backgroundColor = '#FF9800';
ind.innerText = log_prefix + `发生错误 (${msg})`;
} else if (status === 'exec') {
ind.style.backgroundColor = '#2196F3';
ind.innerText = log_prefix + '正在执行指令...';
}
}
function handleError(id, error, errorSource) {
console.error(`${errorSource}错误:`, error);
updateStatus('err', error.message);
const errorMessage = {
type: 'error',
id: id,
sessionId: sid,
error: {
name: error.name,
message: error.message,
stack: error.stack,
source: errorSource
}
};
if (typeof ws !== 'undefined' && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(errorMessage));
} else {
GM_xmlhttpRequest({
method: "POST",
url: httpUrl + "api/result",
headers: {"Content-Type": "application/json"},
data: JSON.stringify(errorMessage),
onload: function(response) {console.log("错误信息已通过HTTP发送", response);},
onerror: function(err) {console.error("发送错误信息失败", err);}
});
}
}
function smartProcessResult(result) {
// 处理 null 和原始类型
if (result === null || result === undefined || typeof result !== 'object') {
return result;
}
// 1. 处理 jQuery 对象 - 强制转换为HTML字符串数组
if (typeof jQuery !== 'undefined' && result instanceof jQuery) {
const elements = [];
for (let i = 0; i < result.length; i++) {
if (result[i] && result[i].nodeType === 1) {
elements.push(result[i].outerHTML);
}
}
return elements; // 始终返回数组
}
// 2. 处理 NodeList 和 HTMLCollection
if (result instanceof NodeList || result instanceof HTMLCollection) {
const elements = [];
for (let i = 0; i < result.length; i++) {
if (result[i] && result[i].nodeType === 1) {
elements.push(result[i].outerHTML);
}
}
return elements;
}
// 3. 处理单个 DOM 元素
if (result.nodeType === 1) {
return result.outerHTML;
}
// 4. 检查是否是具有数字索引和length属性的类数组对象
if (!Array.isArray(result) &&
typeof result === 'object' &&
'length' in result &&
typeof result.length === 'number') {
// 检查第一个元素是否是DOM节点
const firstElement = result[0];
if (firstElement && firstElement.nodeType === 1) {
const elements = [];
const length = Math.min(result.length, 100);
for (let i = 0; i < length; i++) {
const elem = result[i];
if (elem && elem.nodeType === 1) {
elements.push(elem.outerHTML);
}
}
return elements;
}
}
// 5. 处理普通对象和数组 - 使用标准序列化
try {
return JSON.parse(JSON.stringify(result, function(key, value) {
if (typeof value === 'object' && value !== null) {
if (value.nodeType === 1) {
return value.outerHTML;
}
if (value === window || value === document) {
return '[Object]';
}
}
return value;
}));
} catch (e) {
console.error("序列化对象失败:", e);
return `[无法序列化的对象: ${e.message}]`;
}
}
// 防止重复初始化
if (window.ljq_init) return;
window.ljq_init = true;
function connecthttp() {
if (window.use_ws) return;
updateStatus('conn');
GM_xmlhttpRequest({
method: "POST",
url: httpUrl + "api/longpoll",
headers: {"Content-Type": "application/json"},
data: JSON.stringify({
type: 'ready',
url: location.href,
sessionId: sid
}),
onload: async function(resp) {
if (resp.status === 200) {
let data = JSON.parse(resp.responseText);
console.log(log_prefix + '接收到数据:', data);
if (data.id === "" && data.ret === "use ws") return;
if (data.id === "") return setTimeout(connecthttp, 100);
const response = await executeCode(data);
if (response.error) {
handleError(data.id, response.error, '执行代码');
} else {
GM_xmlhttpRequest({
method: "POST",
url: httpUrl + "api/result",
headers: {"Content-Type": "application/json"},
data: JSON.stringify({
type: 'result',
id: data.id,
sessionId: sid,
result: response.result
})
});
}
} else {
console.error(log_prefix + '请求失败,状态码:', resp.status);
updateStatus('err', '请求失败');
}
setTimeout(connecthttp, 1000);
},
onerror: function(err) {
console.error(log_prefix + '请求错误', err);
updateStatus('err', '请求失败');
setTimeout(connecthttp, 5000);
},
ontimeout: function() {
console.log(log_prefix + '请求超时');
updateStatus('err', '请求超时');
setTimeout(connecthttp, 5000);
}
});
}
async function executeCode(data) {
let id = data.id || 'unknown'; // 获取 ID
let result;
if (!data.code) {
console.log('收到非代码执行消息:', data);
return { error: '没有可执行的代码' };
}
updateStatus('exec');
const _open = window.open;
window.open = (url, target, features) => {
GM_openInTab(url, { active: true });
return { success: true, url: url };
};
try {
const jsCode = data.code.trim();
const lines = jsCode.split(/\r?\n/).filter(l => l.trim());
const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : '';
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
if (lastLine.startsWith('return')) {
result = await (new AsyncFunction(jsCode))();
} else {
try {
result = eval(jsCode);
if (result instanceof Promise) result = await result;
} catch (e) {
if (isIllegalReturnError(e) || isAwaitError(e)) {
result = await (new AsyncFunction(jsCode))();
} else throw e;
}
}
const processedResult = smartProcessResult(result);
return { result: processedResult };
} catch (execError) {
return { error: execError };
} finally {
setTimeout(() => window.open = _open, 100);
}
}
function isIllegalReturnError(e) {
return e instanceof SyntaxError && (
/Illegal return statement/i.test(e.message) || // Chrome 常见
/return not in function/i.test(e.message) || // Firefox 常见
/Illegal 'return' statement/i.test(e.message) // 兼容旧文案
);
}
function isAwaitError(e) {
return e instanceof SyntaxError && (
/await is only valid in async/i.test(e.message) || // Chrome
/await.*async/i.test(e.message) // Firefox等
);
}
function connect() {
ws = new WebSocket(wsUrl);
ws.onopen = function() {
window.use_ws = true;
console.log(log_prefix + '已连接');
updateStatus('ok');
ws.send(JSON.stringify({
type: 'ready',
url: location.href,
sessionId: sid
}));
};
ws.onclose = function() {
console.log(log_prefix + '已断开5秒后重连');
updateStatus('disc');
setTimeout(connect, 5000);
};
ws.onerror = function(err) {
console.error(log_prefix + '连接错误', err);
updateStatus('err', '连接失败');
isWebSocketServerAlive(function (e) { if (e) connecthttp()});
};
ws.onmessage = async function(e) {
try {
let data = JSON.parse(e.data);
ws.send(JSON.stringify({type: 'ack',id: data.id}));
const response = await executeCode(data);
if (response.error) {
handleError(data.id, response.error, '执行代码');
} else {
updateStatus('ok');
ws.send(JSON.stringify({
type: 'result',
id: data.id,
sessionId: sid,
result: response.result
}));
}
} catch (parseError) {
handleError('unknown', parseError, '解析消息');
}
};
}
// 初始化
function init() {
if (document.body) {
getIndicator();
connect();
} else {
setTimeout(init, 50);
}
}
// 监控DOM变化 (改为10秒定时器以优化性能)
let indicatorTimer = null;
if (document.readyState !== 'loading') {
init();
indicatorTimer = setInterval(() => getIndicator(), 10000);
} else {
document.addEventListener('DOMContentLoaded', () => {
init();
indicatorTimer = setInterval(() => getIndicator(), 10000);
});
}
// 清理
window.addEventListener('beforeunload', () => {
if (indicatorTimer) clearInterval(indicatorTimer);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
})();

View File

@@ -7,9 +7,10 @@ ljqCtrl Quick Reference:
- Press(cmd, staytime=0): Keyboard shortcuts (e.g. 'ctrl+v')
- FindBlock(fn, wrect=None, threshold=0.8) -> (obj_center_phys, is_found)
- MouseDClick(staytime=0.05), MouseClick(staytime=0.05)
- GrabWindow(hwnd) -> PIL Image: DPI-safe window screenshot
"""
import os, sys, time, random, math, win32api, win32con
import os, sys, time, random, math, win32api, win32con, ctypes
import numpy as np
dpi_scale = 1
@@ -18,24 +19,15 @@ try:
import cv2
except: pass
try:
scr = ImageGrab.grab()
swidth, sheight = scr.size
print('Screen width & height:', swidth, sheight)
cwidth, cheight = map(win32api.GetSystemMetrics, [win32con.SM_CXSCREEN, win32con.SM_CYSCREEN])
dpi_scale = cwidth / swidth
print('dpi_scale:', dpi_scale)
except:
import ctypes
user32 = ctypes.windll.user32
user32.SetProcessDPIAware() # 确保 DPI 感知
cwidth = user32.GetSystemMetrics(0) # SM_CXSCREEN
cheight = user32.GetSystemMetrics(1) # SM_CYSCREEN
try:
dpi = user32.GetDpiForSystem() # Windows 10 1607+
dpi_scale = dpi / 96.0
except: dpi_scale = 1.0 # 降级方案
print(f'Screen (RDP disconnected): {cwidth}x{cheight}, dpi_scale: {dpi_scale}')
_hdc = ctypes.windll.user32.GetDC(0)
swidth = ctypes.windll.gdi32.GetDeviceCaps(_hdc, 118) # DESKTOPHORZRES (物理)
sheight = ctypes.windll.gdi32.GetDeviceCaps(_hdc, 117) # DESKTOPVERTRES
ctypes.windll.user32.ReleaseDC(0, _hdc)
cwidth = win32api.GetSystemMetrics(win32con.SM_CXSCREEN) # 逻辑
cheight = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
dpi_scale = cwidth / swidth
print('Screen width & height:', swidth, sheight)
print('dpi_scale:', dpi_scale)
def MouseDown(): win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
def MouseUp(): win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
@@ -74,6 +66,11 @@ press = Press
VK_CODE = {'backspace':0x08, 'tab':0x09, 'clear':0x0C, 'enter':0x0D, 'shift':0x10, 'ctrl':0x11, 'alt':0x12, 'pause':0x13, 'caps_lock':0x14, 'esc':0x1B, 'escape':0x1B, 'space':0x20, 'page_up':0x21, 'page_down':0x22, 'end':0x23, 'home':0x24, 'left_arrow':0x25, 'up_arrow':0x26, 'right_arrow':0x27, 'down_arrow':0x28, 'select':0x29, 'print':0x2A, 'execute':0x2B, 'print_screen':0x2C, 'ins':0x2D, 'del':0x2E, 'help':0x2F, '0':0x30, '1':0x31, '2':0x32, '3':0x33, '4':0x34, '5':0x35, '6':0x36, '7':0x37, '8':0x38, '9':0x39, 'a':0x41, 'b':0x42, 'c':0x43, 'd':0x44, 'e':0x45, 'f':0x46, 'g':0x47, 'h':0x48, 'i':0x49, 'j':0x4A, 'k':0x4B, 'l':0x4C, 'm':0x4D, 'n':0x4E, 'o':0x4F, 'p':0x50, 'q':0x51, 'r':0x52, 's':0x53, 't':0x54, 'u':0x55, 'v':0x56, 'w':0x57, 'x':0x58, 'y':0x59, 'z':0x5A, 'numpad_0':0x60, 'numpad_1':0x61, 'numpad_2':0x62, 'numpad_3':0x63, 'numpad_4':0x64, 'numpad_5':0x65, 'numpad_6':0x66, 'numpad_7':0x67, 'numpad_8':0x68, 'numpad_9':0x69, 'multiply_key':0x6A, 'add_key':0x6B, 'separator_key':0x6C, 'subtract_key':0x6D, 'decimal_key':0x6E, 'divide_key':0x6F, 'F1':0x70, 'F2':0x71, 'F3':0x72, 'F4':0x73, 'F5':0x74, 'F6':0x75, 'F7':0x76, 'F8':0x77, 'F9':0x78, 'F10':0x79, 'F11':0x7A, 'F12':0x7B, 'F13':0x7C, 'F14':0x7D, 'F15':0x7E, 'F16':0x7F, 'F17':0x80, 'F18':0x81, 'F19':0x82, 'F20':0x83, 'F21':0x84, 'F22':0x85, 'F23':0x86, 'F24':0x87, 'num_lock':0x90, 'scroll_lock':0x91, 'left_shift':0xA0, 'right_shift ':0xA1, 'left_control':0xA2, 'right_control':0xA3, 'left_menu':0xA4, 'right_menu':0xA5, 'browser_back':0xA6, 'browser_forward':0xA7, 'browser_refresh':0xA8, 'browser_stop':0xA9, 'browser_search':0xAA, 'browser_favorites':0xAB, 'browser_start_and_home':0xAC, 'volume_mute':0xAD, 'volume_Down':0xAE, 'volume_up':0xAF, 'next_track':0xB0, 'previous_track':0xB1, 'stop_media':0xB2, 'play/pause_media':0xB3, 'start_mail':0xB4, 'select_media':0xB5, 'start_application_1':0xB6, 'start_application_2':0xB7, 'attn_key':0xF6, 'crsel_key':0xF7, 'exsel_key':0xF8, 'play_key':0xFA, 'zoom_key':0xFB, 'clear_key':0xFE, '+':0xBB, ',':0xBC, '-':0xBD, '.':0xBE, '/':0xBF, '`':0xC0, ';':0xBA, '[':0xDB, '\\':0xDC, ']':0xDD, "'":0xDE, '`':0xC0}
VK_CODE = {k.lower():v for k,v in VK_CODE.items()}
def GrabWindow(hwnd):
import win32gui; win32gui.SetForegroundWindow(hwnd); time.sleep(0.3)
bbox = tuple(int(v / dpi_scale) for v in win32gui.GetWindowRect(hwnd))
return ImageGrab.grab(bbox)
def imshow(mt, sec=0):
cv2.imshow('cc', mt)
cv2.waitKey(sec)