generated from dellevin/template
feat: complete local version to overwrite remote
This commit is contained in:
70
99demo/base64-de-in-code/api.py
Normal file
70
99demo/base64-de-in-code/api.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import base64
|
||||
import proj_utils.utils as utils # 导入你的工具模块
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""渲染主页模板"""
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/en-de-code')
|
||||
def index():
|
||||
"""渲染主页模板"""
|
||||
return render_template('en-de-code-index.html')
|
||||
|
||||
@app.route('/decode', methods=['POST'])
|
||||
def decode_endpoint():
|
||||
"""处理解码请求的 API 端点"""
|
||||
data = request.get_json()
|
||||
base64_input = data.get('input_text', '').strip()
|
||||
|
||||
if not base64_input:
|
||||
return jsonify({'success': False, 'error': '输入不能为空。'})
|
||||
|
||||
try:
|
||||
# 1. Base64 解码
|
||||
decoded_bytes = base64.b64decode(base64_input)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Base64 解码失败: {str(e)}'})
|
||||
|
||||
# 2. 使用 utils 模块中的 detect_and_decode 函数进行编码检测和解码
|
||||
decoded_result = utils.detect_and_decode(decoded_bytes)
|
||||
# 返回成功的 JSON 响应
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'result': decoded_result,
|
||||
})
|
||||
|
||||
@app.route('/encode', methods=['POST'])
|
||||
def encode_endpoint():
|
||||
"""处理编码请求的 API 端点"""
|
||||
data = request.get_json()
|
||||
text_to_encode = data.get('text_to_encode', '')
|
||||
encoding = data.get('encoding', 'utf-8') # 默认使用 UTF-8
|
||||
|
||||
if text_to_encode is None:
|
||||
return jsonify({'success': False, 'error': '输入不能为空。'})
|
||||
|
||||
try:
|
||||
# 1. 根据指定编码格式将文本转换为字节数组
|
||||
encoded_bytes = text_to_encode.encode(encoding)
|
||||
except LookupError:
|
||||
return jsonify({'success': False, 'error': f'未知的编码格式: {encoding}'})
|
||||
except UnicodeEncodeError as e:
|
||||
return jsonify({'success': False, 'error': f'编码失败: {str(e)}'})
|
||||
|
||||
# 2. 将字节数组进行 Base64 编码
|
||||
encoded_string = base64.b64encode(encoded_bytes).decode('ascii')
|
||||
|
||||
# 返回成功的 JSON 响应
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'result': encoded_string
|
||||
})
|
||||
# pip install pipreqs
|
||||
# pipreqs D:\UserData\Desktop\demo\python_script\base64-de-in-code
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
Binary file not shown.
34
99demo/base64-de-in-code/proj_utils/utils.py
Normal file
34
99demo/base64-de-in-code/proj_utils/utils.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import chardet
|
||||
|
||||
# 自动选择解码格式
|
||||
def detect_and_decode(data_bytes):
|
||||
"""使用 chardet 检测编码并解码"""
|
||||
# 使用 chardet 检测编码
|
||||
detected_encoding_info = chardet.detect(data_bytes)
|
||||
detected_encoding = detected_encoding_info.get('encoding')
|
||||
confidence = detected_encoding_info.get('confidence', 0)
|
||||
|
||||
if detected_encoding:
|
||||
try:
|
||||
decoded_string = data_bytes.decode(detected_encoding)
|
||||
print(f" - 使用检测到的编码 '{detected_encoding}' 成功解码,置信度:{confidence}")
|
||||
return decoded_string
|
||||
except UnicodeDecodeError as e:
|
||||
print(f" - 使用检测到的编码 '{detected_encoding}' 解码失败: {e}")
|
||||
|
||||
# 方法二:如果检测失败或置信度低,尝试常见的编码
|
||||
encodings_to_try = ['utf-8', 'gbk', 'gb2312', 'cp936']
|
||||
print(f" - 检测失败/置信度低,尝试常见编码")
|
||||
print(f" - 尝试常见编码列表: {encodings_to_try}")
|
||||
|
||||
for enc in encodings_to_try:
|
||||
try:
|
||||
decoded_string = data_bytes.decode(enc)
|
||||
print(f" - 成功使用编码 '{enc}' 解码。")
|
||||
return decoded_string
|
||||
except UnicodeDecodeError as e:
|
||||
print(f" - 尝试编码 '{enc}' 失败: {e}")
|
||||
continue
|
||||
# 如果所有方法都失败
|
||||
print(" - 所有编码尝试均失败。")
|
||||
return "所有编码尝试均失败。"
|
||||
2
99demo/base64-de-in-code/requirements.txt
Normal file
2
99demo/base64-de-in-code/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
chardet==5.2.0
|
||||
Flask==3.1.2
|
||||
120
99demo/base64-de-in-code/static/style.css
Normal file
120
99demo/base64-de-in-code/static/style.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/* static/style.css */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f6f8fa;
|
||||
color: #24292f;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column; /* 改为列布局 */
|
||||
align-items: center; /* 水平居中 */
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
width: 100%; /* 确保标题占据全宽 */
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 8px 0; /* 减少底部边距 */
|
||||
color: #24292f;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 4px 0;
|
||||
color: #57606a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
width: 90%;
|
||||
max-width: 1600px; /* 增加最大宽度 */
|
||||
gap: 20px; /* 面板间距 */
|
||||
flex: 1; /* 容器占据剩余空间 */
|
||||
height: 60vh; /* 调整容器高度,给标题留出空间 */
|
||||
}
|
||||
|
||||
.panel {
|
||||
flex: 1; /* 左右各占50% */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
|
||||
overflow: hidden; /* 防止内容溢出圆角 */
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
padding: 16px;
|
||||
border: none;
|
||||
resize: none; /* 禁用拖拽调整大小,由JS控制 */
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background-color: #ffffff;
|
||||
color: #24292f;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
min-height: 200px; /* 设置最小高度 */
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
/* Typecho 风格无明显焦点样式,保持简洁 */
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: flex-end; /* 按钮靠右 */
|
||||
gap: 8px; /* 按钮间距 */
|
||||
padding: 12px 16px;
|
||||
background-color: #f6f8fa;
|
||||
border-top: 1px solid #eaecef;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #24292f;
|
||||
background-color: #f6f8fa;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #eaecef;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
background-color: #238636; /* 绿色按钮 */
|
||||
color: white;
|
||||
border-color: #238636;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: #2ea043;
|
||||
}
|
||||
|
||||
button[type="submit"]:active {
|
||||
background-color: #3fb950;
|
||||
}
|
||||
149
99demo/base64-de-in-code/templates/en-de-code-index.html
Normal file
149
99demo/base64-de-in-code/templates/en-de-code-index.html
Normal file
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Base64/编码检测与解码器</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- 页面标题和说明 -->
|
||||
<div class="page-header">
|
||||
<h2>Base64 编码/解码器</h2>
|
||||
<p>因为其他网站base64解码都一坨屎,解码格式一直有问题,无法进行正常解码,要不然就是解码之后中文乱码了,所以写了这个demo</p>
|
||||
<p>接口地址:http(s)://网站域名/decode | http(s)://网站域名/encode</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 左侧面板 (输入) -->
|
||||
<div class="panel">
|
||||
<h2>输入(待编码/解码文本)</h2>
|
||||
<textarea
|
||||
id="inputText"
|
||||
placeholder="在此粘贴待处理的文本..."
|
||||
oninput="autoResize(this)"
|
||||
></textarea>
|
||||
<div class="controls">
|
||||
<button type="button" onclick="clearTextarea('inputText')">清空</button>
|
||||
<button type="button" onclick="process('encode')">编码</button>
|
||||
<button type="button" onclick="process('decode')">解码</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 (输出) -->
|
||||
<div class="panel">
|
||||
<h2>输出</h2>
|
||||
<textarea
|
||||
id="outputResult"
|
||||
readonly
|
||||
placeholder="处理结果将显示在这里..."
|
||||
oninput="autoResize(this)"
|
||||
></textarea>
|
||||
<div class="controls">
|
||||
<button type="button" onclick="clearTextarea('outputResult')">清空</button>
|
||||
<button type="button" onclick="copyResult()">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 自动调整 textarea 高度
|
||||
function autoResize(textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 400) + 'px'; // 最大高度400px
|
||||
}
|
||||
|
||||
// 清空指定的 textarea
|
||||
function clearTextarea(id) {
|
||||
const element = document.getElementById(id);
|
||||
element.value = '';
|
||||
autoResize(element); // 清空后重置高度
|
||||
}
|
||||
|
||||
// 主处理函数 (根据传入的 mode 决定是编码还是解码)
|
||||
async function process(mode) {
|
||||
const inputElement = document.getElementById('inputText');
|
||||
const outputResult = document.getElementById('outputResult');
|
||||
const inputValue = inputElement.value.trim();
|
||||
|
||||
if (!inputValue) {
|
||||
alert('输入不能为空!');
|
||||
return;
|
||||
}
|
||||
|
||||
let endpoint = '';
|
||||
let payload = {};
|
||||
|
||||
if (mode === 'decode') {
|
||||
endpoint = '/decode';
|
||||
payload.input_text = inputValue;
|
||||
} else if (mode === 'encode') {
|
||||
endpoint = '/encode';
|
||||
payload.text_to_encode = inputValue;
|
||||
// 使用默认编码 UTF-8
|
||||
payload.encoding = 'utf-8';
|
||||
} else {
|
||||
alert('未知的处理模式');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
outputResult.value = data.result;
|
||||
autoResize(outputResult); // 处理后调整高度
|
||||
} else {
|
||||
outputResult.value = `错误: ${data.error}`;
|
||||
autoResize(outputResult);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`请求失败:`, error);
|
||||
outputResult.value = `请求失败: ${error.message}`;
|
||||
autoResize(outputResult);
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果到剪贴板
|
||||
function copyResult() {
|
||||
const outputResult = document.getElementById('outputResult');
|
||||
if (outputResult.value === '') {
|
||||
alert('没有内容可复制!');
|
||||
return;
|
||||
}
|
||||
|
||||
outputResult.select();
|
||||
outputResult.setSelectionRange(0, 99999); // 为了移动端兼容
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
// 临时提示
|
||||
const originalText = document.querySelector('.panel:nth-child(2) .controls button:last-child').innerText;
|
||||
document.querySelector('.panel:nth-child(2) .controls button:last-child').innerText = '已复制!';
|
||||
setTimeout(() => {
|
||||
document.querySelector('.panel:nth-child(2) .controls button:last-child').innerText = originalText;
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('复制失败,请手动选择复制。');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('浏览器不支持自动复制,请手动选择复制。');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载后聚焦到输入框
|
||||
window.onload = function() {
|
||||
document.getElementById('inputText').focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
37
99demo/base64-de-in-code/templates/index.html
Normal file
37
99demo/base64-de-in-code/templates/index.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>白荼 - BAITU</title>
|
||||
<!-- 引入 Google Fonts -->
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>恭喜你,来到了一片荒芜之地</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
139
99demo/content-to-tag/extract_doc_tag.py
Normal file
139
99demo/content-to-tag/extract_doc_tag.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
import time # 导入 time 模块
|
||||
|
||||
# OLLAMA_BASE_URL = "http://127.0.0.1:11434/api/generate"
|
||||
# OLLAMA_BASE_URL = "http://152.136.153.72:27009/api/generate"
|
||||
def extract_tags_with_ollama_from_content(ollama_base_url,model, article_content, max_tags=10, min_length=2, max_length=6):
|
||||
"""
|
||||
使用本地 Ollama 大语言模型分析文章内容并提取关键词。
|
||||
|
||||
Args:
|
||||
article_content (str): 输入的文章内容字符串。
|
||||
max_tags (int): 希望返回的最大标签数量。
|
||||
min_length (int): 关键词最小长度。
|
||||
max_length (int): 关键词最大长度。
|
||||
|
||||
Returns:
|
||||
list: 提取到的关键词/标签列表。
|
||||
"""
|
||||
if not article_content or not article_content.strip():
|
||||
return {
|
||||
'code': 0,
|
||||
'message': 'success',
|
||||
'data': {
|
||||
'model': model,
|
||||
'think': '警告:输入的文章内容为空或仅包含空白字符。',
|
||||
'tags': [],
|
||||
'consume': 0.0
|
||||
}
|
||||
}
|
||||
|
||||
# 2. 构建 Prompt
|
||||
# 使用中文提示,更符合 qwen 模型的特点
|
||||
prompt = f"""
|
||||
请严格按照以下要求,从提供的文章内容中提取关键词。
|
||||
|
||||
文章内容:
|
||||
{article_content}
|
||||
|
||||
要求:
|
||||
- 提取最多 {max_tags} 个最能概括文章主旨和核心概念的关键词。
|
||||
- 关键词必须来源于文章内容,准确反映文章主题。
|
||||
- 每个关键词的长度必须在 {min_length} 到 {max_length} 个字符之间。
|
||||
- 输出格式为:关键词1, 关键词2, 关键词3, ...
|
||||
- 只输出关键词列表,不要有任何其他解释或前缀。
|
||||
|
||||
"""
|
||||
# 记录开始时间
|
||||
start_time = time.time()
|
||||
# 3. 准备发送给 Ollama API 的 payload
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"system": "你是一个专门生成文章标签的助手,请你根据我给你的文章的内容总结并生成一系列的标签,格式可以参考[关键词1, 关键词2, 关键词3].你只需要给我生成这种形式的标签即可,其他分析内容无需输出.",
|
||||
"stream": False,
|
||||
"options": {
|
||||
"top_p": 0.9,
|
||||
"temperature": 0.1, # 较低的温度使输出更确定、更聚焦
|
||||
"num_predict": 64000, # 控制预测的最大 token 数
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
# 4. 发送 POST 请求到 Ollama API
|
||||
response = requests.post(ollama_base_url, json=payload)
|
||||
# 5. 检查响应状态
|
||||
if response.status_code != 200:
|
||||
# print(f"Error: Ollama API returned status code {response.status_code}")
|
||||
print(response.text)
|
||||
return {
|
||||
'code': 404,
|
||||
'message': f"{response['error']}",
|
||||
}
|
||||
# 6. 解析 JSON 响应
|
||||
result = response.json()
|
||||
if "response" not in result:
|
||||
print(result)
|
||||
return {
|
||||
'code': 500,
|
||||
'message': "Error: Unexpected response format from Ollama",
|
||||
}
|
||||
|
||||
llm_output = result["response"].strip()
|
||||
# print(llm_output)
|
||||
think_match = re.search(r'<think>(.*?)</think>', llm_output, re.DOTALL)
|
||||
ai_think = think_match.group(1).strip() if think_match else ""
|
||||
# 移除 <think>...</think> 标签及其内容,得到纯净的关键词列表部分
|
||||
clean_output = re.sub(r'<think>.*?</think>', '', llm_output, count=1, flags=re.DOTALL).strip()
|
||||
# 7. 简单清洗和验证关键词
|
||||
# 假设 LLM 输出格式为 "关键词1, 关键词2, ..."
|
||||
raw_tags = [tag.strip() for tag in clean_output.split(',') if tag.strip()]
|
||||
# print(raw_tags)
|
||||
# 过滤掉不符合长度要求的词
|
||||
# filtered_tags = [
|
||||
# tag for tag in raw_tags
|
||||
# if min_length <= len(tag) <= max_length and tag # 忽略空字符串
|
||||
# ]
|
||||
# 去重并保持顺序
|
||||
seen = set()
|
||||
unique_filtered_tags = []
|
||||
for tag in raw_tags:
|
||||
if tag not in seen:
|
||||
seen.add(tag)
|
||||
unique_filtered_tags.append(tag)
|
||||
|
||||
# 记录结束时间
|
||||
end_time = time.time()
|
||||
# 计算耗时
|
||||
elapsed_time = end_time - start_time
|
||||
return {
|
||||
'code':0,
|
||||
'message': 'success',
|
||||
'data': {
|
||||
'model': model,
|
||||
'think': ai_think,
|
||||
'tags': unique_filtered_tags,
|
||||
'consume': elapsed_time
|
||||
}
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
# print(f"Error calling Ollama API: {e}")
|
||||
return {
|
||||
'code': 500,
|
||||
'message': f"Error calling Ollama API: {e}",
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
# print(f"Error decoding JSON response from Ollama: {e}")
|
||||
return {
|
||||
'code': 500,
|
||||
'message': f"Error decoding JSON response from Ollama: {e}"
|
||||
}
|
||||
except Exception as e:
|
||||
# print(f"An unexpected error occurred: {e}")
|
||||
return {
|
||||
'code': 500,
|
||||
'message': f"An unexpected error occurred: {e}"
|
||||
}
|
||||
40
99demo/content-to-tag/main.py
Normal file
40
99demo/content-to-tag/main.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import extract_doc_tag as ex_doc_tag
|
||||
|
||||
# --- 方式一:指定文件路径 ---
|
||||
file_path = 'test.txt'
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
my_article_content = file.read()
|
||||
except FileNotFoundError:
|
||||
print(f"错误:找不到文件 '{file_path}'")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"读取文件时发生错误: {e}")
|
||||
exit(1)
|
||||
|
||||
model = "qwen3:1.7b"
|
||||
# model = "qwen3:0.6b"
|
||||
# OLLAMA_BASE_URL = "http://127.0.0.1:11434/api/generate"
|
||||
OLLAMA_BASE_URL = "http://152.136.153.72:27009/api/generate"
|
||||
extracted_tags = ex_doc_tag.extract_tags_with_ollama_from_content(
|
||||
OLLAMA_BASE_URL,
|
||||
model,
|
||||
my_article_content,
|
||||
max_tags=5,
|
||||
min_length=2,
|
||||
max_length=10
|
||||
)
|
||||
|
||||
if extracted_tags['code'] == 0:
|
||||
print('思考过程:')
|
||||
print(extracted_tags['data']['think'])
|
||||
print('=' * 60)
|
||||
print('文章内容长度', len(my_article_content))
|
||||
# print(f"Ollama 模型:{model}")
|
||||
print('=' * 60)
|
||||
print('文章标签:', extracted_tags['data']['tags'])
|
||||
print('=' * 60)
|
||||
print(f"总耗时: {extracted_tags['data']['consume']:.2f} 秒")
|
||||
else:
|
||||
print(extracted_tags)
|
||||
13
99demo/content-to-tag/test.txt
Normal file
13
99demo/content-to-tag/test.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
今天周六早上起床,心血来潮开始刷视频,不知不觉时间就像被偷走一样,和游戏类似,两三个小时转瞬即逝。我每次都想着,诶呀一个一分多钟的视频算什么,下一个!于是在百个视频过去后一上午的时间也消失了。
|
||||
|
||||
从喜羊羊,到情感生活,到家庭育儿,到程序编码,到机器制作,再到科幻制作,再到大黄蜂。你可能会奇怪,喜羊羊和情感生活有什么关系,家庭育儿和程序编码有什么关系,机器制作和科幻制作又有什么关系。其实这些没有奇怪的地方,喜羊羊里面的喜羊羊美羊羊沸羊羊的关系引出情感,儿童的幼年编程引出了程序编码,机器制作的炫酷效果引出了科幻制作。
|
||||
|
||||
一个视频就像细胞上面的无数的受体和另一个细胞上面的配体一样,他们那种概念的联系将一个个视频连锁成宇宙中的群星看似毫无联系,细看却是万般变化的星座。
|
||||
|
||||
我在想是什么变成这样的呢?标签?模糊的标签?也许是这样,但这这是一种浅显的理解吧!因为这种标签,把他们分成了不同的种类,划分成不同的视频,然后用一个概念展示给我们看。罗翔老师讲过一个他的一件故事,他在上大学的时候,初入大学很多一个地区的人会互相认识,他去了他们省的聚会,然后他突然发现下次的聚合没有叫他,才知道是一个市区的聚会。具体内容我早已忘记,但是大概意思是这样的。我们每个人都像网络视频那样被打上了标签,我们用这个来区分自己的同类,异类。我们会和同类笑着说“看啊!那个人说话好怪啊!看啊!那个人吃饭的样子好丑”但是也许你在你同类眼里也是个异类,一个嘲笑他人的异类。于是你不断给自己打标签,做完这个做那个,加入这个加那个,最终像一只蝙蝠一样,鸟兽不分。
|
||||
|
||||
扯远了。其实这种标签带来的分化自古就有,可以具象的堪称阶级划分,就像盖茨比赚了再多的钱,举办了再多的宴会也融入不进去权贵阶层。可以抽象的理解为人们的闲话,谁家怎么怎么了,那家又如何如何。这种东西就像是无形的线,勒住我们的思想和手指。大数据的判别让我们困于信息茧房,让我们只看到自己想看的(比如追星的人会刷到自己喜欢明星的一切,不喜欢那个明星的则会刷到各种搞怪的视频)。我们的思想逐渐变得狭隘,三分钟的视频让我们的喜欢和伤心都会变得特别廉价。我经常看到的一句话就是“原本很开心的,看到你这里绷不住了”,“挺难受的,你这评论给我笑嘻了”无数的视频过去,让我们的喜欢随便的说出口,让我们的痛苦说来就来。
|
||||
|
||||
很难不否认短视频在释放压力方面是对当代打工人的良药。快节奏+短视频带给无数人不一样的生活见地,可是这种方式终归是饮鸩止渴,浅陋而自大的。你再也不会记得白鹿原里的白嘉轩,只会明白他叫大壮,我们再也不会记得南海十三郎里面的江誉镠,也许他的名字会叫作小亮。真不知百年之后人们的思想会是如何。
|
||||
|
||||
我相信还是会有下一个鲁迅的,那种《中国人难道失掉自信力了吗》那种俯首甘为孺子牛的鲁迅。用笔尖刺痛每一人的脊梁。也许我们不是愚蠢,只是不想醒罢了
|
||||
BIN
99demo/down-video/__pycache__/main.cpython-312.pyc
Normal file
BIN
99demo/down-video/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
2951
99demo/down-video/bilibili_cookies.txt
Normal file
2951
99demo/down-video/bilibili_cookies.txt
Normal file
File diff suppressed because it is too large
Load Diff
241
99demo/down-video/main.py
Normal file
241
99demo/down-video/main.py
Normal file
@@ -0,0 +1,241 @@
|
||||
import yt_dlp
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import queue
|
||||
import threading
|
||||
import subprocess
|
||||
from urllib.parse import urlparse
|
||||
from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_file
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
DEFAULT_OUTPUT_DIR = r"D:\UserData\Desktop\xiazai"
|
||||
TWITTER_COOKIE = "x_cookies.txt"
|
||||
BILIBILI_COOKIE = "bilibili_cookies.txt"
|
||||
PROXY_URL = "socks5://127.0.0.1:10808"
|
||||
|
||||
|
||||
def check_ffmpeg():
|
||||
"""检查 FFmpeg 是否可用"""
|
||||
try:
|
||||
result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_twitter_url(url):
|
||||
"""检查是否是 Twitter/X 链接"""
|
||||
domain = urlparse(url).netloc.lower()
|
||||
return 'x.com' in domain or 'twitter.com' in domain
|
||||
|
||||
|
||||
def is_bilibili_url(url):
|
||||
"""检查是否是 Bilibili 链接"""
|
||||
domain = urlparse(url).netloc.lower()
|
||||
return 'bilibili.com' in domain or 'b23.tv' in domain
|
||||
|
||||
|
||||
def _find_files_by_title(output_dir, safe_title):
|
||||
"""根据安全标题查找目录中的相关文件"""
|
||||
return [
|
||||
f for f in os.listdir(output_dir)
|
||||
if f.startswith(safe_title) and os.path.isfile(os.path.join(output_dir, f))
|
||||
]
|
||||
|
||||
|
||||
def _merge_mp4_m4a(output_dir, safe_title):
|
||||
"""
|
||||
查找同名的 .mp4 和 .m4a 并用 FFmpeg 合并。
|
||||
返回 (merged: bool, message: str, filepath: str)
|
||||
"""
|
||||
files = _find_files_by_title(output_dir, safe_title)
|
||||
mp4_files = [f for f in files if f.endswith('.mp4')]
|
||||
m4a_files = [f for f in files if f.endswith('.m4a')]
|
||||
|
||||
if not mp4_files or not m4a_files:
|
||||
return False, "未检测到需要合并的 .mp4 + .m4a"
|
||||
|
||||
# 取最可能匹配的一对:文件名最短(不含 quality suffix 的通常最短)
|
||||
mp4_files.sort(key=len)
|
||||
m4a_files.sort(key=len)
|
||||
mp4_path = os.path.join(output_dir, mp4_files[0])
|
||||
m4a_path = os.path.join(output_dir, m4a_files[0])
|
||||
|
||||
# 合并输出文件名
|
||||
merged_name = safe_title + "_merged.mp4"
|
||||
merged_path = os.path.join(output_dir, merged_name)
|
||||
counter = 1
|
||||
while os.path.exists(merged_path):
|
||||
merged_name = f"{safe_title}_merged_{counter}.mp4"
|
||||
merged_path = os.path.join(output_dir, merged_name)
|
||||
counter += 1
|
||||
|
||||
cmd = [
|
||||
'ffmpeg', '-y',
|
||||
'-i', mp4_path,
|
||||
'-i', m4a_path,
|
||||
'-c', 'copy',
|
||||
merged_path
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode == 0 and os.path.exists(merged_path):
|
||||
# 合并成功后删除原始分片
|
||||
try:
|
||||
os.remove(mp4_path)
|
||||
os.remove(m4a_path)
|
||||
except Exception:
|
||||
pass
|
||||
return True, f"已合并为 {merged_name}", merged_path
|
||||
else:
|
||||
return False, f"FFmpeg 合并失败: {result.stderr[:200]}", ""
|
||||
except Exception as e:
|
||||
return False, f"合并异常: {str(e)}", ""
|
||||
|
||||
|
||||
def download_video(video_url, output_dir, platform):
|
||||
"""
|
||||
通用下载函数,返回 (success: bool, message: str, title: str, filepath: str)
|
||||
"""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
ydl_opts = {
|
||||
'outtmpl': os.path.join(output_dir, '%(title)s.%(ext)s'),
|
||||
'ignoreerrors': True,
|
||||
}
|
||||
|
||||
if platform == 'twitter':
|
||||
cookie_file = TWITTER_COOKIE
|
||||
if PROXY_URL:
|
||||
ydl_opts['proxy'] = PROXY_URL
|
||||
elif platform == 'bilibili':
|
||||
cookie_file = BILIBILI_COOKIE
|
||||
# Bilibili 不使用代理,且需要合并配置
|
||||
ydl_opts['merge_output_format'] = 'mp4'
|
||||
ydl_opts['postprocessors'] = [{
|
||||
'key': 'FFmpegVideoConvertor',
|
||||
'preferedformat': 'mp4',
|
||||
}]
|
||||
else:
|
||||
return False, "不支持的平台", "", ""
|
||||
|
||||
if os.path.exists(cookie_file):
|
||||
ydl_opts['cookiefile'] = cookie_file
|
||||
else:
|
||||
return False, f"Cookie 文件 '{cookie_file}' 未找到", "", ""
|
||||
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(video_url, download=True)
|
||||
if not info:
|
||||
return False, "未能获取视频信息,下载可能失败", "", ""
|
||||
|
||||
title = info.get('title', 'Unknown Title')
|
||||
safe_title = re.sub(r'[<>:\"/\\|?*]', '_', title)
|
||||
files = _find_files_by_title(output_dir, safe_title)
|
||||
|
||||
if not files:
|
||||
return False, "下载完成但未找到输出文件", title, ""
|
||||
|
||||
# 确定最终视频文件路径
|
||||
# 优先取合并后的文件,否则取最大的 mp4 文件
|
||||
filepath = ""
|
||||
candidate_files = [f for f in files if f.endswith('.mp4')]
|
||||
if candidate_files:
|
||||
# 优先 merged 文件
|
||||
merged_candidates = [f for f in candidate_files if '_merged' in f]
|
||||
if merged_candidates:
|
||||
filepath = os.path.join(output_dir, merged_candidates[0])
|
||||
else:
|
||||
# 取最新/最大的文件
|
||||
candidate_files.sort(key=lambda f: os.path.getsize(os.path.join(output_dir, f)), reverse=True)
|
||||
filepath = os.path.join(output_dir, candidate_files[0])
|
||||
else:
|
||||
# 没有 mp4,取任意文件
|
||||
files.sort(key=lambda f: os.path.getsize(os.path.join(output_dir, f)), reverse=True)
|
||||
filepath = os.path.join(output_dir, files[0])
|
||||
|
||||
# Bilibili 后处理:如果存在分离的 mp4 + m4a,手动合并
|
||||
extra_msg = ""
|
||||
if platform == 'bilibili':
|
||||
has_mp4 = any(f.endswith('.mp4') for f in files)
|
||||
has_m4a = any(f.endswith('.m4a') for f in files)
|
||||
if has_mp4 and has_m4a:
|
||||
merged, merge_msg, merged_path = _merge_mp4_m4a(output_dir, safe_title)
|
||||
extra_msg = f" ({merge_msg})" if merge_msg else ""
|
||||
if merged and merged_path:
|
||||
filepath = merged_path
|
||||
|
||||
return True, f"下载成功!保存到 {output_dir}{extra_msg}", title, filepath
|
||||
|
||||
except Exception as e:
|
||||
return False, f"下载失败: {str(e)}", "", ""
|
||||
|
||||
|
||||
@app.route("/file")
|
||||
def serve_file():
|
||||
"""提供视频文件访问和下载"""
|
||||
filepath = request.args.get("path", "")
|
||||
as_download = request.args.get("download", "0") == "1"
|
||||
|
||||
if not filepath:
|
||||
return "Missing path", 400
|
||||
|
||||
# 安全检查:确保是绝对路径且文件存在
|
||||
filepath = os.path.abspath(filepath)
|
||||
if not os.path.exists(filepath) or not os.path.isfile(filepath):
|
||||
return "File not found", 404
|
||||
|
||||
# 只允许访问视频文件(防止任意文件读取)
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
if ext not in ('.mp4', '.m4a', '.webm', '.mkv', '.mov', '.avi'):
|
||||
return "File type not allowed", 403
|
||||
|
||||
return send_file(filepath, as_attachment=as_download)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
ffmpeg_ok = check_ffmpeg()
|
||||
return render_template("index.html", ffmpeg_ok=ffmpeg_ok)
|
||||
|
||||
|
||||
@app.route("/download", methods=["POST"])
|
||||
def download():
|
||||
data = request.get_json()
|
||||
url = data.get("url", "").strip() if data else ""
|
||||
output_dir = data.get("output_dir", DEFAULT_OUTPUT_DIR).strip() if data else DEFAULT_OUTPUT_DIR
|
||||
|
||||
if not url:
|
||||
return jsonify({"success": False, "message": "请输入视频链接"}), 400
|
||||
|
||||
if is_twitter_url(url):
|
||||
platform = 'twitter'
|
||||
elif is_bilibili_url(url):
|
||||
platform = 'bilibili'
|
||||
else:
|
||||
return jsonify({"success": False, "message": "仅支持 Twitter/X 和 Bilibili 视频链接"}), 400
|
||||
|
||||
if not output_dir:
|
||||
output_dir = DEFAULT_OUTPUT_DIR
|
||||
|
||||
success, message, title, filepath = download_video(url, output_dir, platform)
|
||||
return jsonify({"success": success, "message": message, "title": title, "filepath": filepath})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Twitter / Bilibili 视频下载器")
|
||||
print("=" * 50)
|
||||
if check_ffmpeg():
|
||||
print("FFmpeg 已检测到")
|
||||
else:
|
||||
print("警告: 未检测到 FFmpeg")
|
||||
print(f"默认保存目录: {DEFAULT_OUTPUT_DIR}")
|
||||
print("访问 http://127.0.0.1:5000 打开下载页面")
|
||||
print("=" * 50)
|
||||
app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)
|
||||
380
99demo/down-video/templates/index.html
Normal file
380
99demo/down-video/templates/index.html
Normal file
@@ -0,0 +1,380 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>视频下载器</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #f5f5f5;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
/* 左侧面板 */
|
||||
.left-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #222;
|
||||
min-width: 420px;
|
||||
}
|
||||
.input-section {
|
||||
padding: 32px;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
.input-section h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: #fff;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.input-section .subtitle {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.input-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 10px;
|
||||
background: #0a0a0a;
|
||||
color: #f5f5f5;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.input-group input::placeholder { color: #444; }
|
||||
.input-group input:focus { border-color: #fff; background: #000; }
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
button:hover:not(:disabled) { background: #e0e0e0; transform: translateY(-1px); }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status.show { display: block; }
|
||||
.status.success { background: #0a1f0a; color: #4ade80; border: 1px solid #1a3a1a; }
|
||||
.status.error { background: #1f0a0a; color: #f87171; border: 1px solid #3a1a1a; }
|
||||
.status.info { background: #0a0a1f; color: #60a5fa; border: 1px solid #1a1a3a; }
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255,255,255,0.15);
|
||||
border-top-color: #60a5fa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.footer-meta {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: #444;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.badge.ok { background: #0a1f0a; color: #4ade80; border: 1px solid #1a3a1a; }
|
||||
.badge.warn { background: #1f1a0a; color: #fbbf24; border: 1px solid #3a301a; }
|
||||
|
||||
/* 日志区域 */
|
||||
.log-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.log-header {
|
||||
padding: 14px 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.log-panel {
|
||||
flex: 1;
|
||||
padding: 20px 32px;
|
||||
background: #000;
|
||||
color: #4ade80;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 右侧面板 */
|
||||
.right-panel {
|
||||
flex: 1.2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.right-panel .placeholder {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
.right-panel .placeholder svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.right-panel .placeholder p {
|
||||
font-size: 14px;
|
||||
}
|
||||
.video-wrap {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
height: 100%;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.video-wrap.show { display: flex; }
|
||||
.video-player {
|
||||
max-width: 100%;
|
||||
max-height: calc(100% - 70px);
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
background: #000;
|
||||
border: 1px solid #222;
|
||||
outline: none;
|
||||
}
|
||||
.video-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.video-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.video-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.countdown {
|
||||
font-size: 12px;
|
||||
color: #f87171;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.btn-download {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border: 1px solid #333;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-download:hover { background: #2a2a2a; border-color: #444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="left-panel">
|
||||
<div class="input-section">
|
||||
<h1>视频下载</h1>
|
||||
<p class="subtitle">支持 X 和 Bilibili</p>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="url">视频链接</label>
|
||||
<input type="url" id="url" placeholder="https://x.com/... 或 https://www.bilibili.com/video/...">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="outputDir">保存路径</label>
|
||||
<input type="text" id="outputDir" value="D:\UserData\Desktop\xiazai">
|
||||
</div>
|
||||
|
||||
<button id="downloadBtn" onclick="startDownload()">开始下载</button>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
|
||||
<div class="footer-meta">
|
||||
<span>FFmpeg</span>
|
||||
{% if ffmpeg_ok %}
|
||||
<span class="badge ok">已就绪</span>
|
||||
{% else %}
|
||||
<span class="badge warn">未检测到</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-section" style="display:none;">
|
||||
<div class="log-header">
|
||||
<span>控制台输出</span>
|
||||
<span id="logStatus" style="font-size:11px;color:#444;">等待开始</span>
|
||||
</div>
|
||||
<pre id="logPanel" class="log-panel"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div id="placeholder" class="placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="2" y="2" width="20" height="20" rx="4"/>
|
||||
<polygon points="9,8 17,12 9,16"/>
|
||||
</svg>
|
||||
<p>下载完成后在此预览视频</p>
|
||||
</div>
|
||||
|
||||
<div id="videoWrap" class="video-wrap">
|
||||
<video id="videoPlayer" class="video-player" controls></video>
|
||||
<div class="video-info">
|
||||
<div id="videoTitle" class="video-title"></div>
|
||||
<div class="video-actions">
|
||||
<a id="downloadLink" class="btn-download" href="#" target="_blank">下载文件</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const urlInput = document.getElementById('url');
|
||||
const btn = document.getElementById('downloadBtn');
|
||||
const statusEl = document.getElementById('status');
|
||||
const logPanel = document.getElementById('logPanel');
|
||||
const logStatus = document.getElementById('logStatus');
|
||||
const placeholder = document.getElementById('placeholder');
|
||||
const videoWrap = document.getElementById('videoWrap');
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
const videoTitle = document.getElementById('videoTitle');
|
||||
const countdownEl = document.getElementById('countdown');
|
||||
const downloadLink = document.getElementById('downloadLink');
|
||||
|
||||
function showStatus(text, type) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = 'status show ' + type;
|
||||
}
|
||||
function clearStatus() { statusEl.className = 'status'; }
|
||||
|
||||
function showVideo(filepath, title) {
|
||||
placeholder.style.display = 'none';
|
||||
videoWrap.classList.add('show');
|
||||
videoPlayer.src = '/file?path=' + encodeURIComponent(filepath);
|
||||
videoTitle.textContent = title || '未命名视频';
|
||||
downloadLink.href = '/file?path=' + encodeURIComponent(filepath) + '&download=1';
|
||||
downloadLink.setAttribute('download', '');
|
||||
}
|
||||
|
||||
async function startDownload() {
|
||||
const url = urlInput.value.trim();
|
||||
const outputDir = document.getElementById('outputDir').value.trim();
|
||||
if (!url) {
|
||||
showStatus('请输入视频链接', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
clearStatus();
|
||||
showStatus('', 'info');
|
||||
statusEl.innerHTML = '<span class="spinner"></span>正在下载,请稍候...';
|
||||
statusEl.className = 'status show info';
|
||||
|
||||
// 隐藏之前的视频
|
||||
videoWrap.classList.remove('show');
|
||||
placeholder.style.display = 'block';
|
||||
videoPlayer.src = '';
|
||||
|
||||
try {
|
||||
const res = await fetch('/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: url, output_dir: outputDir })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showStatus((data.title ? '《' + data.title + '》' : '') + ' 下载完成', 'success');
|
||||
if (data.filepath) {
|
||||
showVideo(data.filepath, data.title);
|
||||
}
|
||||
} else {
|
||||
showStatus(data.message, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('请求失败: ' + err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
urlInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') startDownload();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
35
99demo/down-video/x_cookies.txt
Normal file
35
99demo/down-video/x_cookies.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# This file is generated by yt-dlp. Do not edit.
|
||||
|
||||
zhutix.com FALSE / FALSE 1782047186 X_CACHE_KEY 0291346395e7ad60497281e3e38d4231
|
||||
.x.com TRUE / FALSE 1808747867 __cuid b00c4a6e73cf471e95c093aec1d1c0b0
|
||||
.x.com TRUE / TRUE 1807414323 personalization_id "v1_X+OVgjnn20lE7ppYGHrXzQ=="
|
||||
.x.com TRUE / TRUE 1807414324 guest_id v1%3A177285432573681062
|
||||
.x.com TRUE / TRUE 1807414357 kdt aYTMkoEta2sR2Cbrf9TCU2vL6NQrNTk6xP26HzJA
|
||||
.x.com TRUE / TRUE 1807414357 auth_token e0a16f1e58541f609e0b26bfd2759c5508ad67c6
|
||||
.x.com TRUE / TRUE 1807414357 ct0 0699cddc947b9fa7e4b226e70d7fc69fdd46a9d4623aea18b98cf8ed9d99fd308a01fec0e531ae1bb50b602a0ba9e87691d694888c39440c7bc8bdc3e35afd1d0757903d5bc702b71fa81c648a0108e7
|
||||
.x.com TRUE / TRUE 1808748895 guest_id_ads v1%3A177285432573681062
|
||||
.x.com TRUE / TRUE 1808748895 guest_id_marketing v1%3A177285432573681062
|
||||
.x.com TRUE / TRUE 1805724895 twid u%3D1051785185509822466
|
||||
.x.com TRUE / TRUE 1777215667 __cf_bm k7mwLxnd0mtyuyQLThqb20Oyb3q2.yEJfZw570X8CEI-1777213867.2375512-1.0.1.1-GAwpDeSYeFaRo9HzHK2Xf5tYtXiD4SzQj_L0kFw1S1P7FZWwSnOuJZ2JfxMxlG7vnUjDS0lBbszszPUWdOFb0upzl1YbBdE0jf6q0t7nJB0qvN9MElSi3lTTFOdf7HEs
|
||||
www.dropbox.com FALSE / TRUE 1790090452 gvc MTk3NzU0NTUwMjg0MDYzMDYzMzA4NTQ0MjEyNzAzODM1MzUxNTYz
|
||||
www.dropbox.com FALSE / TRUE 1787066452 __Host-js_csrf ee7vG1QZzX2hhBoptjGNMkhg
|
||||
.dropbox.com TRUE / TRUE 1787066452 t ee7vG1QZzX2hhBoptjGNMkhg
|
||||
.netflix.com TRUE / FALSE 1789916349 nfvdid BQFmAAEBEKpkhEFXmQMTd61zb4rT6FdApPuZS-qvyP3HIzl9d5Tk2rjxE6wW5b--LFBw-Ebbcmp7rJ640s9P91ltSiy8DTzu-0gEy3Ob8B_JHniDavIQXQ%3D%3D
|
||||
.netflix.com TRUE / TRUE 1789916349 SecureNetflixId v%3D3%26mac%3DAQEAEQABABSSBFH4KvE6qPl_-6UI_re1LAdlfAy3rZk.%26dt%3D1758380348892
|
||||
.netflix.com TRUE / TRUE 1789916349 NetflixId v%3D3%26ct%3DBgjHlOvcAxLAAb-lG8nEmlB-x4EMpdZG6gG0_ZGe7HgQvUbBUgbe6kPr6Or-1cC94UaN0RbSzxx_OYBaaHLg2MvdKRvw0dKlWwzRJyychkiKImYRxA1PHETG8KjG86tx5smLKn9QcETtqecvyrKl04PUFZ6KuO8Y3LrCO8posa-VMj5U3gonTjruh63uy_zFQYQygkvrJIShQ1yQjNxCropJEzK536vs46y7MYjt_KPCIK9E8tpT80whmRDQ7DgBniSOyGTtiKKyFhgGIg4KDEsJyQjnvm9ZXXKI7g..
|
||||
.yandex.com TRUE / TRUE 1795878312 yuidss 3388071441761318311
|
||||
.yandex.com TRUE / TRUE 1795878312 is_gdpr 0
|
||||
.yandex.com TRUE / TRUE 1795878312 yandexuid 3388071441761318311
|
||||
.yandex.com TRUE / TRUE 1806578318 my YwA=
|
||||
.yandex.com TRUE / TRUE 1795878652 is_gdpr_b CMWxMBD63gIoAg==
|
||||
.yandex.com TRUE / TRUE 1806578314 i Tl5C1iTbvBz58B4Pigqm2d8QgUC+4wYSpsK4t9kymsq2s0hX0eVihjDkvNmLxPLd8yXet7JjFuW1h5SeT3bJ+65Aepo=
|
||||
.yandex.com TRUE / TRUE 1806846551 yp 2087646552.pcs.0#1792854544.swntab.0#1787786326.szm.1:1920x1080:1304x732:15#1774610321.ygu.1#1772882321.dlp.2
|
||||
.yandex.com TRUE / TRUE 1806848331 _yasc RfaPS2g6xVw7Omhl+7/4pXIp30Wti0n8MwSiKMV6meOJl3Phq4TlP15XKqkV+MVJhr3OKmKyvTnS0m3SuMVVFhSLDPGFpWauk4yqACQuAydoupgmirYCoa16gRa9L3hZv/kc4QnkyqvC3E2KLQ==
|
||||
.yandex.com TRUE / TRUE 1806848512 bh EkEiQ2hyb21pdW0iO3Y9IjE0MCIsICJOb3Q9QT9CcmFuZCI7dj0iMjQiLCAiR29vZ2xlIENocm9tZSI7dj0iMTQwIhoFIng4NiIiECIxNDAuMC43MzM5LjEyOCIqAj8wMgIiIjoJIldpbmRvd3MiQggiMTkuMC4wIkoEIjY0IlJdIkNocm9taXVtIjt2PSIxNDAuMC43MzM5LjEyOCIsICJOb3Q9QT9CcmFuZCI7dj0iMjQuMC4wLjAiLCAiR29vZ2xlIENocm9tZSI7dj0iMTQwLjAuNzMzOS4xMjgiWgI/MGDD84vNBmoe3Mrh/wiS2KGxA5/P4eoD+/rw5w3r//32D/68z4cI
|
||||
www.kninebox.com FALSE / FALSE 1800025226 SITE_TOTAL_ID c9a65e54de60ac4fe330c0ff3782502c
|
||||
www.zabbix.com FALSE / TRUE 1798733253 bblastvisit 1767197255
|
||||
www.zabbix.com FALSE / TRUE 1798733263 bblastactivity 1767197264
|
||||
x.com FALSE / FALSE 1788406355 g_state {"i_l":0,"i_ll":1772854333755,"i_e":{"enable_itp_optimization":0}}
|
||||
x.com FALSE / FALSE 0 lang zh-cn
|
||||
www.wmmflix.com FALSE / FALSE 1805607768 lastvisit 121%091774071767%09%2Fapp-index-run%3Fapp%3Dsearch%26keywords%3Dtt0817177
|
||||
687
99demo/pin-img/pin-tu.py
Normal file
687
99demo/pin-img/pin-tu.py
Normal file
@@ -0,0 +1,687 @@
|
||||
# app.py
|
||||
from flask import Flask, render_template_string, send_from_directory, request
|
||||
import os
|
||||
import re
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
# --- 配置 ---
|
||||
# 请在这里修改为你存放图片的文件夹路径
|
||||
ROOT_IMAGE_FOLDER = r'\\192.168.31.92\ubuntuShare2T\学习资料' # 使用原始字符串以避免转义问题
|
||||
PORT = 80 # 你可以修改这个端口号,如果5000被占用了
|
||||
# --- 配置结束 ---
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def atoi(text):
|
||||
"""辅助函数:将文本中的数字部分转换为整数,用于排序"""
|
||||
return int(text) if text.isdigit() else text
|
||||
|
||||
|
||||
def natural_keys(text):
|
||||
"""
|
||||
辅助函数:生成用于自然排序的键。
|
||||
例如: '1002' -> ['1002'] (其中 '1002' 被转换为整数)
|
||||
'img_10' -> ['img_', 10]
|
||||
这样可以确保 '1001.jpg' 排在 '1002.png' 之前。
|
||||
"""
|
||||
return [atoi(c) for c in re.split(r'(\d+)', text)]
|
||||
|
||||
|
||||
def find_media_in_folder(folder_path):
|
||||
"""在指定文件夹中查找所有图片和视频文件"""
|
||||
allowed_image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
|
||||
allowed_video_exts = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mkv', '.mpg', '.mpeg', '.3gp',
|
||||
'.3g2'}
|
||||
|
||||
try:
|
||||
files = os.listdir(folder_path)
|
||||
except PermissionError:
|
||||
print(f"权限不足,无法访问: {folder_path}")
|
||||
return [], []
|
||||
|
||||
image_files = [f for f in files if os.path.isfile(os.path.join(folder_path, f)) and os.path.splitext(f)[
|
||||
1].lower() in allowed_image_exts]
|
||||
video_files = [f for f in files if os.path.isfile(os.path.join(folder_path, f)) and os.path.splitext(f)[
|
||||
1].lower() in allowed_video_exts]
|
||||
|
||||
return sorted(image_files, key=natural_keys), sorted(video_files, key=natural_keys)
|
||||
|
||||
|
||||
def get_subfolders(folder_path):
|
||||
"""获取指定文件夹下的所有子文件夹"""
|
||||
try:
|
||||
items = os.listdir(folder_path)
|
||||
except PermissionError:
|
||||
print(f"权限不足,无法访问: {folder_path}")
|
||||
return []
|
||||
subfolders = [item for item in items if os.path.isdir(os.path.join(folder_path, item))]
|
||||
return sorted(subfolders, key=natural_keys)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# 默认访问根目录,subpath为空字符串
|
||||
return list_folder_content('')
|
||||
|
||||
|
||||
@app.route('/browse/')
|
||||
@app.route('/browse/<path:subpath>')
|
||||
def list_folder_content(subpath=''):
|
||||
"""
|
||||
递归浏览文件夹内容的主函数
|
||||
subpath: URL中传递的子路径
|
||||
"""
|
||||
# 从查询参数获取显示模式,默认为 'list'
|
||||
mode = request.args.get('mode', 'list')
|
||||
if mode not in ['list', 'grid', 'manga']:
|
||||
mode = 'list'
|
||||
|
||||
# 构建当前要浏览的物理路径
|
||||
# 注意:这里使用全局变量 ROOT_IMAGE_FOLDER
|
||||
current_path = os.path.normpath(os.path.join(ROOT_IMAGE_FOLDER, subpath))
|
||||
|
||||
# 检测是否为漫画路径
|
||||
is_manga_path = '漫画' in subpath
|
||||
|
||||
# 安全检查,防止路径穿越
|
||||
if not current_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
|
||||
return "Access Denied", 403
|
||||
|
||||
# 查找当前文件夹下的媒体文件和子文件夹
|
||||
image_files, video_files = find_media_in_folder(current_path)
|
||||
subfolders = get_subfolders(current_path)
|
||||
|
||||
# 构建面包屑导航
|
||||
path_parts = [p for p in subpath.split('/') if p]
|
||||
breadcrumbs = [{'name': 'Home', 'url': '/'}]
|
||||
cumulative_path = ''
|
||||
for part in path_parts:
|
||||
cumulative_path += part + '/'
|
||||
breadcrumbs.append({'name': part, 'url': f'/browse/{cumulative_path.rstrip("/")}'})
|
||||
|
||||
html_template = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>文件浏览器 - {{ current_path_name }}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
/* 顶部地址栏 - Windows风格 */
|
||||
.address-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 8px 16px;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.address-bar a, .address-bar span.current {
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.address-bar a:hover {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
.address-bar span.current {
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
}
|
||||
.address-bar .sep {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 0 2px;
|
||||
user-select: none;
|
||||
}
|
||||
.address-bar .home-btn {
|
||||
font-size: 16px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
gap: 12px;
|
||||
}
|
||||
.mode-btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid #ccc;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.mode-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.mode-btn.active {
|
||||
background: #0078d4;
|
||||
color: #fff;
|
||||
border-color: #0078d4;
|
||||
}
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
/* 网格模式 */
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.grid-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-item:hover {
|
||||
border-color: #0078d4;
|
||||
background: #f0f8ff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.grid-item .icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
.grid-item img.thumb {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.grid-item .name {
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
max-height: 2.8em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
/* 列表模式 */
|
||||
.list-container {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.list-item:last-child { border-bottom: none; }
|
||||
.list-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.list-item .icon {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.list-item .thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
.list-item .info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.list-item .name {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.list-item .type {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
/* Modal / Lightbox */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.modal-content {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
.modal-content video {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
background: #000;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.modal-close:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
.modal-title {
|
||||
color: #fff;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
max-width: 80vw;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.img-trigger, .video-trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 漫画模式 */
|
||||
.manga-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #1a1a1a;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.manga-item {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
line-height: 0;
|
||||
}
|
||||
.manga-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 地址栏 -->
|
||||
<div class="address-bar">
|
||||
<a href="/" class="home-btn" title="主页">🏠</a>
|
||||
{% for crumb in breadcrumbs %}
|
||||
{% if not loop.first %}
|
||||
<span class="sep">›</span>
|
||||
{% if loop.last %}
|
||||
<span class="current">{{ crumb.name }}</span>
|
||||
{% else %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.name }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<a href="?mode=list" class="mode-btn {% if mode == 'list' %}active{% endif %}">📋 列表</a>
|
||||
<a href="?mode=grid" class="mode-btn {% if mode == 'grid' %}active{% endif %}">⊞ 网格</a>
|
||||
{% if is_manga_path %}
|
||||
<a href="?mode=manga" class="mode-btn {% if mode == 'manga' %}active{% endif %}">📖 漫画</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 文件夹 -->
|
||||
{% if subfolders %}
|
||||
<div class="section">
|
||||
<div class="section-title"><span>📁</span> 文件夹 <span style="color:#999;font-weight:400;">({{ subfolders|length }})</span></div>
|
||||
{% if mode == 'grid' %}
|
||||
<div class="grid-container">
|
||||
{% for folder in subfolders %}
|
||||
<a href="{{ url_for('list_folder_content', subpath=(current_subpath + folder) if current_subpath else folder) }}?mode={{ mode }}" class="grid-item">
|
||||
<div class="icon">📁</div>
|
||||
<div class="name">{{ folder }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="list-container">
|
||||
{% for folder in subfolders %}
|
||||
<a href="{{ url_for('list_folder_content', subpath=(current_subpath + folder) if current_subpath else folder) }}?mode={{ mode }}" class="list-item">
|
||||
<div class="icon">📁</div>
|
||||
<div class="info">
|
||||
<div class="name">{{ folder }}</div>
|
||||
<div class="type">文件夹</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 图片 -->
|
||||
{% if images %}
|
||||
<div class="section">
|
||||
<div class="section-title"><span>📷</span> 图片 <span style="color:#999;font-weight:400;">({{ images|length }})</span></div>
|
||||
{% if mode == 'manga' %}
|
||||
<div class="manga-container">
|
||||
{% for image in images %}
|
||||
<div class="manga-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
|
||||
<img src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="{{ image }}" loading="lazy">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif mode == 'grid' %}
|
||||
<div class="grid-container">
|
||||
{% for image in images %}
|
||||
<div class="grid-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
|
||||
<img class="thumb" src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="{{ image }}" loading="lazy">
|
||||
<div class="name">{{ image }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="list-container">
|
||||
{% for image in images %}
|
||||
<div class="list-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
|
||||
<img class="thumb" src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="" loading="lazy">
|
||||
<div class="info">
|
||||
<div class="name">{{ image }}</div>
|
||||
<div class="type">图片文件</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 视频 -->
|
||||
{% if videos %}
|
||||
<div class="section">
|
||||
<div class="section-title"><span>🎬</span> 视频 <span style="color:#999;font-weight:400;">({{ videos|length }})</span></div>
|
||||
{% if mode == 'grid' %}
|
||||
<div class="grid-container">
|
||||
{% for video in videos %}
|
||||
<div class="grid-item video-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + video) if current_subpath else video) }}" data-name="{{ video }}">
|
||||
<div class="icon">🎬</div>
|
||||
<div class="name">{{ video }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="list-container">
|
||||
{% for video in videos %}
|
||||
<div class="list-item video-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + video) if current_subpath else video) }}" data-name="{{ video }}">
|
||||
<div class="icon">🎬</div>
|
||||
<div class="info">
|
||||
<div class="name">{{ video }}</div>
|
||||
<div class="type">视频文件</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 图片/视频预览弹窗 -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<div class="modal-close" onclick="closeModal()">×</div>
|
||||
<img id="modalImg" style="display:none;" alt="">
|
||||
<video id="modalVideo" style="display:none;" controls playsinline></video>
|
||||
<div class="modal-title" id="modalTitle"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const overlay = document.getElementById('modalOverlay');
|
||||
const modalImg = document.getElementById('modalImg');
|
||||
const modalVideo = document.getElementById('modalVideo');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
|
||||
function openImage(src, name) {
|
||||
modalVideo.style.display = 'none';
|
||||
modalVideo.pause();
|
||||
modalVideo.src = '';
|
||||
modalImg.style.display = 'block';
|
||||
modalImg.src = src;
|
||||
modalTitle.textContent = name || '';
|
||||
overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function openVideo(src, name) {
|
||||
modalImg.style.display = 'none';
|
||||
modalImg.src = '';
|
||||
modalVideo.style.display = 'block';
|
||||
modalVideo.src = src;
|
||||
modalVideo.play();
|
||||
modalTitle.textContent = name || '';
|
||||
overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeModal(e) {
|
||||
if (e && e.target !== overlay && !e.target.classList.contains('modal-close')) return;
|
||||
overlay.classList.remove('active');
|
||||
modalVideo.pause();
|
||||
modalVideo.src = '';
|
||||
modalImg.src = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.img-trigger').forEach(el => {
|
||||
el.addEventListener('click', () => openImage(el.dataset.src, el.dataset.name));
|
||||
});
|
||||
document.querySelectorAll('.video-trigger').forEach(el => {
|
||||
el.addEventListener('click', () => openVideo(el.dataset.src, el.dataset.name));
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# 获取当前路径的名称用于显示
|
||||
current_path_name = os.path.basename(current_path) if current_path != ROOT_IMAGE_FOLDER else 'Root Directory'
|
||||
|
||||
return render_template_string(
|
||||
html_template,
|
||||
images=image_files,
|
||||
videos=video_files,
|
||||
subfolders=subfolders,
|
||||
current_path_name=current_path_name,
|
||||
current_subpath=subpath + '/' if subpath else '', # 传递给模板,用于构建文件路径
|
||||
breadcrumbs=breadcrumbs,
|
||||
mode=mode, # 将模式传递给模板
|
||||
is_manga_path=is_manga_path
|
||||
)
|
||||
|
||||
|
||||
# 新增:用于在新页面播放视频的路由
|
||||
@app.route('/play_video/<path:full_path>')
|
||||
def play_video(full_path):
|
||||
"""
|
||||
在新页面播放单个视频
|
||||
full_path: 视频文件的完整路径(包含子文件夹)
|
||||
"""
|
||||
# 安全检查
|
||||
full_file_path = os.path.normpath(os.path.join(ROOT_IMAGE_FOLDER, full_path))
|
||||
if not full_file_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
|
||||
return "Access Denied", 403
|
||||
|
||||
# 获取视频文件名
|
||||
video_filename = os.path.basename(full_path)
|
||||
|
||||
# 将 video_url 的生成移到模板内部
|
||||
video_player_template = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>正在播放: {{ video_filename }}</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #000; /* 黑色背景 */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh; /* 全屏高度 */
|
||||
}
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 90%; /* 最大宽度 */
|
||||
max-height: 90vh; /* 最大高度 */
|
||||
}
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain; /* 保持视频比例 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="video-container">
|
||||
<video class="video-player" controls autoplay playsinline preload="none">
|
||||
<!-- 在模板内部使用 url_for -->
|
||||
<source src="{{ url_for('serve_media', full_path=full_path) }}" type="video/{{ video_filename.split('.')[-1].lower() }}">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
# 注意:传递给 render_template_string 的变量,可以在模板内部直接使用
|
||||
return render_template_string(
|
||||
video_player_template,
|
||||
video_filename=video_filename,
|
||||
full_path=full_path # 将 full_path 也传递给模板,以便在模板内使用 url_for
|
||||
)
|
||||
|
||||
|
||||
# 统一的媒体文件服务路由,处理图片和视频
|
||||
@app.route('/media_file/<path:full_path>')
|
||||
def serve_media(full_path):
|
||||
"""
|
||||
提供媒体文件(图片/视频)服务
|
||||
full_path: 包含子文件夹路径和文件名
|
||||
"""
|
||||
# 从完整路径中分离出文件夹和文件名
|
||||
folder_path = os.path.dirname(full_path)
|
||||
filename = os.path.basename(full_path)
|
||||
|
||||
# 构建完整的物理路径
|
||||
base_dir = os.path.join(ROOT_IMAGE_FOLDER, folder_path)
|
||||
|
||||
# 安全检查
|
||||
full_file_path = os.path.normpath(os.path.join(base_dir, filename))
|
||||
if not full_file_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
|
||||
return "Access Denied", 403
|
||||
|
||||
return send_from_directory(base_dir, filename)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 确保根图片文件夹存在
|
||||
if not os.path.exists(ROOT_IMAGE_FOLDER):
|
||||
print(f"错误:找不到根图片文件夹 '{ROOT_IMAGE_FOLDER}'")
|
||||
exit(1)
|
||||
|
||||
print(f"Web服务即将启动,请访问 http://127.0.0.1:{PORT}")
|
||||
print(f"根目录路径: {os.path.abspath(ROOT_IMAGE_FOLDER)}")
|
||||
app.run(debug=True, host='0.0.0.0', port=PORT)
|
||||
301
99demo/tokenizer-demo/api.py
Normal file
301
99demo/tokenizer-demo/api.py
Normal file
@@ -0,0 +1,301 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import re
|
||||
import jieba
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# --- 日志配置 ---
|
||||
# 创建一个logger
|
||||
logger = logging.getLogger('user_activity')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# 创建一个handler,用于写入日志文件
|
||||
file_handler = logging.FileHandler('useruse.log', mode='a', encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# 创建一个handler,用于输出到控制台(可选)
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# 定义日志格式
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# 给logger添加handler
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# --- 从文件加载自定义词汇 ---
|
||||
JIEBA_DICT_FILE = 'jieba.txt'
|
||||
|
||||
|
||||
def load_custom_words_from_file(file_path):
|
||||
"""从指定文件加载自定义词汇"""
|
||||
custom_words = set() # 使用set避免重复
|
||||
if not os.path.exists(file_path):
|
||||
print(f"警告: 找不到自定义词汇文件 '{file_path}',将使用空列表。")
|
||||
return list(custom_words), set()
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
word = line.strip() # 去除行首行尾的空白字符和换行符
|
||||
if word: # 确保不是空行
|
||||
custom_words.add(word)
|
||||
except Exception as e:
|
||||
print(f"错误: 读取自定义词汇文件 '{file_path}' 时发生异常: {e}")
|
||||
|
||||
# 从加载的词汇中筛选出需要特殊处理的复合词(包含大写字母的驼峰命名)
|
||||
compound_words = {word for word in custom_words if any(c.isupper() for c in word)}
|
||||
simple_custom_words = custom_words - compound_words
|
||||
|
||||
return list(simple_custom_words), compound_words
|
||||
|
||||
|
||||
# 从文件加载自定义词汇
|
||||
SIMPLE_CUSTOM_WORDS, COMPOUND_WORDS_SET = load_custom_words_from_file(JIEBA_DICT_FILE)
|
||||
|
||||
# 为简单词汇添加到jieba词典
|
||||
for word in SIMPLE_CUSTOM_WORDS:
|
||||
jieba.add_word(word)
|
||||
# print(f"成功从 '{JIEBA_DICT_FILE}' 加载了 {len(SIMPLE_CUSTOM_WORDS)} 个简单自定义词汇。")
|
||||
# print(f"识别出 {len(COMPOUND_WORDS_SET)} 个复合词汇待处理: {list(COMPOUND_WORDS_SET)[:10]}...") # 打印前10个作为示例
|
||||
|
||||
# 预编译复合词的正则表达式模式,以提高性能
|
||||
# 例如: (MemoFlow|MookNote|VoceChat)
|
||||
COMPOUND_PATTERN = re.compile('|'.join(re.escape(w) for w in COMPOUND_WORDS_SET), re.IGNORECASE)
|
||||
|
||||
|
||||
def tokenize_text(text):
|
||||
"""
|
||||
对中英文及数字进行分词
|
||||
- 复合词:优先匹配自定义的驼峰命名复合词
|
||||
- 中文:使用 jieba 分词
|
||||
- 英文:按空格和标点分词
|
||||
- 数字:单独提取
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return []
|
||||
|
||||
tokens = []
|
||||
# 定义正则表达式模式
|
||||
# 中文字符范围
|
||||
chinese_pattern = re.compile(r'[\u4e00-\u9fff]+')
|
||||
# 英文单词(包含连字符)
|
||||
english_pattern = re.compile(r'[a-zA-Z]+(?:-[a-zA-Z]+)*')
|
||||
# 数字(包含小数、负数)
|
||||
number_pattern = re.compile(r'-?\d+\.?\d*')
|
||||
|
||||
# 当前处理位置
|
||||
pos = 0
|
||||
text_length = len(text)
|
||||
|
||||
while pos < text_length:
|
||||
char = text[pos]
|
||||
|
||||
# 跳过空白字符
|
||||
if char.isspace():
|
||||
pos += 1
|
||||
continue
|
||||
|
||||
# --- 新增逻辑:优先匹配复合词 ---
|
||||
compound_match = COMPOUND_PATTERN.match(text, pos)
|
||||
if compound_match:
|
||||
matched_word = compound_match.group()
|
||||
tokens.append({
|
||||
'text': matched_word,
|
||||
'type': 'compound_eng',
|
||||
'length': len(matched_word)
|
||||
})
|
||||
pos += len(matched_word)
|
||||
continue # 匹配到复合词后,跳过后续的其他匹配
|
||||
|
||||
# 匹配中文字符串
|
||||
if chinese_pattern.match(char):
|
||||
# 找到连续的中文字符
|
||||
chinese_str = ''
|
||||
while pos < text_length and chinese_pattern.match(text[pos]):
|
||||
chinese_str += text[pos]
|
||||
pos += 1
|
||||
|
||||
# 使用 jieba 对中文字符串进行分词
|
||||
# 注意:这里的 jieba.cut() 会使用我们之前 add_word 添加的自定义词汇
|
||||
chinese_tokens = list(jieba.cut(chinese_str))
|
||||
for token in chinese_tokens:
|
||||
if token.strip():
|
||||
tokens.append({
|
||||
'text': token,
|
||||
'type': 'chinese',
|
||||
'length': len(token)
|
||||
})
|
||||
|
||||
# 匹配英文单词
|
||||
elif english_pattern.match(char):
|
||||
match = english_pattern.match(text, pos)
|
||||
if match:
|
||||
word = match.group()
|
||||
tokens.append({
|
||||
'text': word,
|
||||
'type': 'english',
|
||||
'length': len(word)
|
||||
})
|
||||
pos += len(word)
|
||||
|
||||
# 匹配数字
|
||||
elif number_pattern.match(char) or (char == '-' and pos + 1 < text_length and text[pos + 1].isdigit()):
|
||||
match = number_pattern.match(text, pos)
|
||||
if match:
|
||||
number = match.group()
|
||||
tokens.append({
|
||||
'text': number,
|
||||
'type': 'number',
|
||||
'length': len(number)
|
||||
})
|
||||
pos += len(number)
|
||||
|
||||
# 其他字符(标点符号等),单独处理
|
||||
else:
|
||||
tokens.append({
|
||||
'text': char,
|
||||
'type': 'punctuation',
|
||||
'length': 1
|
||||
})
|
||||
pos += 1
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
def get_token_stats(tokens):
|
||||
"""获取分词统计信息"""
|
||||
stats = {
|
||||
'total': len(tokens),
|
||||
'chinese': 0,
|
||||
'english': 0,
|
||||
'compound_eng': 0, # 为复合词类型添加计数
|
||||
'number': 0,
|
||||
'punctuation': 0
|
||||
}
|
||||
|
||||
for token in tokens:
|
||||
token_type = token['type']
|
||||
if token_type in stats:
|
||||
stats[token_type] += 1
|
||||
|
||||
# 重新计算总数,确保准确性
|
||||
stats['total'] = sum(v for k, v in stats.items() if k != 'total')
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# 定义用户Token文件路径
|
||||
TOKEN_FILE_PATH = 'usertoken.json'
|
||||
|
||||
|
||||
# 启动时加载Token文件
|
||||
def load_valid_tokens():
|
||||
"""从文件加载有效的Token列表"""
|
||||
if not os.path.exists(TOKEN_FILE_PATH):
|
||||
print(f"错误: 找不到Token文件 '{TOKEN_FILE_PATH}'")
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(TOKEN_FILE_PATH, 'r', encoding='utf-8') as f:
|
||||
tokens_data = json.load(f)
|
||||
|
||||
# 将列表转换为以token为键的字典,便于快速查找
|
||||
token_map = {}
|
||||
for item in tokens_data:
|
||||
# 确保数据格式正确
|
||||
if 'user' in item and 'token' in item:
|
||||
token_map[item['token']] = item['user']
|
||||
else:
|
||||
print(f"警告: Token文件中发现格式错误的条目: {item}")
|
||||
|
||||
# print(f"成功从 '{TOKEN_FILE_PATH}' 加载了 {len(token_map)} 个有效Token。")
|
||||
return token_map
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"错误: Token文件 '{TOKEN_FILE_PATH}' 不是有效的JSON格式。{e}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"错误: 读取Token文件时发生异常: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# 程序启动时加载一次
|
||||
VALID_TOKENS_MAP = load_valid_tokens()
|
||||
|
||||
|
||||
def get_user_by_token(token):
|
||||
"""根据token获取用户名,如果无效则返回None"""
|
||||
return VALID_TOKENS_MAP.get(token)
|
||||
|
||||
|
||||
def authenticate_request():
|
||||
"""检查请求头中的Authorization token,并返回用户信息"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return None, "缺少或格式错误的Authorization头。请使用 'Authorization: Bearer <your_token>'"
|
||||
|
||||
token = auth_header.split(" ", 1)[1]
|
||||
user = get_user_by_token(token)
|
||||
|
||||
if not user:
|
||||
return None, "无效的Token"
|
||||
|
||||
return user, None
|
||||
|
||||
|
||||
@app.route('/tokenize', methods=['POST'])
|
||||
def tokenize_endpoint():
|
||||
"""处理分词请求的 API 端点"""
|
||||
user, error_msg = authenticate_request()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': error_msg}), 401
|
||||
|
||||
data = request.get_json()
|
||||
input_text = data.get('input_text', '')
|
||||
|
||||
# 记录日志:谁在什么时候输入了什么内容
|
||||
log_message = f"User '{user}' submitted text: '{input_text}'"
|
||||
logger.info(log_message)
|
||||
print(f"Log Recorded: {log_message}") # 控制台也打印一下,方便即时查看
|
||||
|
||||
try:
|
||||
# 执行分词
|
||||
tokens = tokenize_text(input_text)
|
||||
|
||||
# 获取统计信息
|
||||
stats = get_token_stats(tokens)
|
||||
|
||||
# 返回成功的 JSON 响应,包含请求的用户信息
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'tokens': tokens,
|
||||
'stats': stats,
|
||||
'original_text': input_text,
|
||||
'requested_by': user # 添加请求者信息
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# 如果处理出错,也记录一条错误日志
|
||||
error_log_message = f"Error processing request for user '{user}': {str(e)}"
|
||||
logger.error(error_log_message)
|
||||
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'分词处理失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
# --- 以下为原有的分词和统计逻辑,保持不变 ---
|
||||
# (tokenize_text 和 get_token_stats 已经在上面定义过了)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=False, host='0.0.0.0', port=27056) # 生产环境建议关闭debug
|
||||
62
99demo/tokenizer-demo/example_client.py
Normal file
62
99demo/tokenizer-demo/example_client.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
分词接口调用示例
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
|
||||
BASE_URL = "http://127.0.0.1:27056"
|
||||
# BASE_URL = "http://fenci.iletter.top/"
|
||||
|
||||
|
||||
def tokenize_single(text):
|
||||
"""单文本分词"""
|
||||
url = f"{BASE_URL}/tokenize"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer DdzBgb8GEkBpA8gtCJXP24hGJz9bXpEOi6z91fHm25X59q5968XqtLxPi1MfiTHJ"
|
||||
}
|
||||
payload = {"input_text": text}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
print(response.json())
|
||||
return response.json()
|
||||
|
||||
|
||||
def tokenize_batch(texts):
|
||||
"""批量分词"""
|
||||
url = f"{BASE_URL}/batch-tokenize"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer DdzBgb8GEkBpA8gtCJXP24hGJz9bXpEOi6z91fHm25X59q5968XqtLxPi1MfiTHJ"
|
||||
}
|
||||
payload = {"texts": texts}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例 1: 单文本分词
|
||||
print("=" * 50)
|
||||
print("示例 1: 单文本分词")
|
||||
print("=" * 50)
|
||||
|
||||
text = "MemoFlowMookNoteVoceChat"
|
||||
result = tokenize_single(text)
|
||||
|
||||
if result["success"]:
|
||||
print(f"原文: {result['original_text']}")
|
||||
print(f"\n分词结果:")
|
||||
for token in result["tokens"]:
|
||||
print(f" [{token['type']:12}] {token['text']}")
|
||||
|
||||
print(f"\n统计信息:")
|
||||
stats = result["stats"]
|
||||
print(f" 总计: {stats['total']} 个单词")
|
||||
print(f" 中文: {stats['chinese']} 个")
|
||||
print(f" 英文: {stats['english']} 个")
|
||||
print(f" 数字: {stats['number']} 个")
|
||||
print(f" 标点: {stats['punctuation']} 个")
|
||||
else:
|
||||
print(f"错误: {result['error']}")
|
||||
|
||||
37
99demo/tokenizer-demo/jieba.txt
Normal file
37
99demo/tokenizer-demo/jieba.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
MemoFlow
|
||||
MookNote
|
||||
VoceChat
|
||||
WireGuard
|
||||
SyncClipboard
|
||||
floccus
|
||||
GitNex
|
||||
Bitwarden
|
||||
Immich
|
||||
牛逼
|
||||
安益强
|
||||
张昊洋
|
||||
王美丽
|
||||
李冰素
|
||||
清华大学
|
||||
人工智能学院
|
||||
自然语言处理
|
||||
机器学习算法
|
||||
康嘉超
|
||||
王震瀛
|
||||
张钰
|
||||
贝利亚
|
||||
朱益广
|
||||
好的
|
||||
小程序
|
||||
问一下
|
||||
曹婷婷
|
||||
好滴
|
||||
鲁桂娟
|
||||
安兰兰
|
||||
李春雨
|
||||
王娜娜
|
||||
telegram
|
||||
Telegram
|
||||
v2rayNG
|
||||
tool
|
||||
root
|
||||
2
99demo/tokenizer-demo/requirements.txt
Normal file
2
99demo/tokenizer-demo/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Flask==3.1.2
|
||||
jieba==0.42.1
|
||||
6
99demo/tokenizer-demo/usertoken.json
Normal file
6
99demo/tokenizer-demo/usertoken.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"user": "admin",
|
||||
"token": "DdzBgb8GEkBpA8gtCJXP24hGJz9bXpEOi6z91fHm25X59q5968XqtLxPi1MfiTHJ"
|
||||
}
|
||||
]
|
||||
6
99demo/tokenizer-demo/useruse.log
Normal file
6
99demo/tokenizer-demo/useruse.log
Normal file
@@ -0,0 +1,6 @@
|
||||
2026-03-26 19:23:28,016 - user_activity - INFO - User 'admin' submitted text: '安益强,张昊洋,张文灵,李冰素,王一'
|
||||
2026-03-26 19:31:03,289 - user_activity - INFO - User 'admin' submitted text: '山东省信用金桥中小企业综合服务平台'
|
||||
2026-03-26 19:34:07,902 - user_activity - INFO - User 'admin' submitted text: '瘦了10斤了,我真牛逼。暂时不考虑收徒~'
|
||||
2026-03-26 19:35:47,899 - user_activity - INFO - User 'admin' submitted text: '瘦了10斤了,我真牛逼。暂时不考虑收徒~'
|
||||
2026-03-26 20:04:52,096 - user_activity - INFO - User 'admin' submitted text: 'MemoFlowMookNoteVoceChat'
|
||||
2026-03-26 20:08:06,572 - user_activity - INFO - User 'admin' submitted text: 'MemoFlowMookNoteVoceChat'
|
||||
Reference in New Issue
Block a user