import
re
import
time
import
aiohttp
import
asyncio
import
tkinter as tk
import
threading
from
tkinter
import
ttk, filedialog, scrolledtext, messagebox
from
typing
import
List
,
Tuple
class
ConfigWindow(tk.Tk):
def
__init__(
self
):
super
().__init__()
self
.title(
"DeepSeek 故事分析器"
)
self
.geometry(
"720x700"
)
self
.protocol(
"WM_DELETE_WINDOW"
,
self
.on_close)
self
.api_key
=
tk.StringVar(value
=
"请输入硅基流动中的API密钥!"
)
self
.input_file
=
tk.StringVar()
self
.output_dir
=
tk.StringVar()
self
.concurrency
=
tk.IntVar(value
=
3
)
self
.prompt_template
=
tk.StringVar(
value
=
"请以简洁的纯文本格式输出故事八要素,包含:身份、目的、行动、核心问题、阻力、结局、正能量和负能量,不要修饰。"
)
self
.content_length
=
tk.IntVar(value
=
2000
)
self
.temperature
=
tk.DoubleVar(value
=
0.3
)
self
.connect_timeout
=
tk.IntVar(value
=
35
)
self
.total_timeout
=
tk.IntVar(value
=
160
)
self
.processing
=
False
self
.processor
=
None
self
.create_widgets()
self
.setup_log_area()
def
create_widgets(
self
):
row
=
0
ttk.Label(
self
, text
=
"输入文件:"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Entry(
self
, textvariable
=
self
.input_file, width
=
50
).grid(row
=
row, column
=
1
)
ttk.Button(
self
, text
=
"浏览..."
, command
=
self
.select_input_file).grid(row
=
row, column
=
2
)
row
+
=
1
ttk.Label(
self
, text
=
"输出目录:"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Entry(
self
, textvariable
=
self
.output_dir, width
=
50
).grid(row
=
row, column
=
1
)
ttk.Button(
self
, text
=
"浏览..."
, command
=
self
.select_output_dir).grid(row
=
row, column
=
2
)
row
+
=
1
ttk.Label(
self
, text
=
"API密钥:"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Entry(
self
, textvariable
=
self
.api_key, width
=
50
).grid(row
=
row, column
=
1
, columnspan
=
2
)
row
+
=
1
ttk.Label(
self
, text
=
"并发数 (1-5):"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Spinbox(
self
, from_
=
1
, to
=
5
, textvariable
=
self
.concurrency, width
=
5
).grid(row
=
row, column
=
1
, sticky
=
"w"
)
row
+
=
1
ttk.Label(
self
, text
=
"每章截取长度:"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Spinbox(
self
, from_
=
100
, to
=
5000
, increment
=
100
, textvariable
=
self
.content_length, width
=
8
).grid(row
=
row,
column
=
1
,
sticky
=
"w"
)
row
+
=
1
ttk.Label(
self
, text
=
"温度值 (0.1-1):"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Spinbox(
self
, from_
=
0.1
, to
=
1.0
, increment
=
0.1
,
format
=
"%.1f"
,
textvariable
=
self
.temperature, width
=
5
).grid(row
=
row, column
=
1
, sticky
=
"w"
)
row
+
=
1
ttk.Label(
self
, text
=
"超时设置(秒):"
).grid(row
=
row, column
=
0
, sticky
=
"w"
, padx
=
10
, pady
=
5
)
ttk.Frame(
self
).grid(row
=
row, column
=
1
, sticky
=
"w"
)
ttk.Label(
self
, text
=
"连接"
).grid(row
=
row, column
=
1
, sticky
=
"w"
)
ttk.Spinbox(
self
, from_
=
10
, to
=
300
, textvariable
=
self
.connect_timeout, width
=
5
).grid(row
=
row, column
=
2
,
sticky
=
"w"
, padx
=
5
)
ttk.Label(
self
, text
=
"总超时"
).grid(row
=
row, column
=
3
, sticky
=
"w"
)
ttk.Spinbox(
self
, from_
=
30
, to
=
600
, textvariable
=
self
.total_timeout, width
=
5
).grid(row
=
row, column
=
4
,
sticky
=
"w"
)
row
+
=
1
ttk.Label(
self
, text
=
"提示模板:"
).grid(row
=
row, column
=
0
, sticky
=
"nw"
, padx
=
10
, pady
=
5
)
self
.prompt_editor
=
scrolledtext.ScrolledText(
self
, width
=
70
, height
=
8
, wrap
=
tk.WORD)
self
.prompt_editor.insert(
"1.0"
,
self
.prompt_template.get())
self
.prompt_editor.grid(row
=
row, column
=
1
, columnspan
=
4
, pady
=
5
, sticky
=
"ew"
)
row
+
=
1
self
.btn_frame
=
ttk.Frame(
self
)
self
.btn_frame.grid(row
=
row, column
=
1
, pady
=
10
, sticky
=
"e"
)
self
.start_btn
=
ttk.Button(
self
.btn_frame, text
=
"开始处理"
, command
=
self
.toggle_processing)
self
.start_btn.pack(side
=
"left"
, padx
=
5
)
row
+
=
1
def
setup_log_area(
self
):
self
.log_area
=
scrolledtext.ScrolledText(
self
, width
=
85
, height
=
12
, state
=
"disabled"
)
self
.log_area.grid(row
=
10
, column
=
0
, columnspan
=
5
, padx
=
10
, pady
=
5
, sticky
=
"nsew"
)
self
.grid_rowconfigure(
10
, weight
=
1
)
def
select_input_file(
self
):
filepath
=
filedialog.askopenfilename(filetypes
=
[(
"文本文件"
,
"*.txt"
)])
if
filepath:
self
.input_file.
set
(filepath)
def
select_output_dir(
self
):
dirpath
=
filedialog.askdirectory()
if
dirpath:
self
.output_dir.
set
(dirpath)
def
toggle_processing(
self
):
if
not
self
.processing:
self
.start_processing()
else
:
self
.cancel_processing()
def
start_processing(
self
):
if
not
all
([
self
.input_file.get(),
self
.output_dir.get(),
self
.api_key.get()]):
messagebox.showerror(
"错误"
,
"请填写所有必填字段!"
)
return
try
:
if
not
(
0.1
<
=
self
.temperature.get() <
=
1.0
):
raise
ValueError(
"温度值必须在0.1到1.0之间"
)
if
self
.content_length.get() <
100
:
raise
ValueError(
"截取长度至少100字符"
)
except
Exception as e:
messagebox.showerror(
"参数错误"
,
str
(e))
return
self
.processing
=
True
self
.start_btn.config(text
=
"取消处理"
)
self
.log_area.config(state
=
"normal"
)
self
.log_area.delete(
"1.0"
, tk.END)
self
.log_area.config(state
=
"disabled"
)
self
.processor
=
TurboProcessor(
api_key
=
self
.api_key.get(),
input_file
=
self
.input_file.get(),
output_dir
=
self
.output_dir.get(),
concurrency
=
self
.concurrency.get(),
prompt_template
=
self
.prompt_editor.get(
"1.0"
, tk.END).strip(),
content_length
=
self
.content_length.get(),
temperature
=
self
.temperature.get(),
timeout
=
(
self
.connect_timeout.get(),
self
.total_timeout.get()),
log_callback
=
self
.update_log
)
self
.thread
=
threading.Thread(target
=
self
.run_async_task, daemon
=
True
)
self
.thread.start()
def
cancel_processing(
self
):
if
self
.processor:
self
.processor.cancel()
self
.processing
=
False
self
.start_btn.config(text
=
"开始处理"
)
self
.update_log(
"处理已中止"
)
def
run_async_task(
self
):
try
:
loop
=
asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(
self
.processor.run())
except
Exception as e:
self
.update_log(f
"错误: {str(e)}"
)
finally
:
self
.after(
0
,
lambda
:
self
.cancel_processing())
def
update_log(
self
, message:
str
):
self
.log_area.config(state
=
"normal"
)
self
.log_area.insert(tk.END, message
+
"\n"
)
self
.log_area.see(tk.END)
self
.log_area.config(state
=
"disabled"
)
def
on_close(
self
):
if
self
.processing:
if
messagebox.askokcancel(
"退出"
,
"处理正在进行中,确定要退出吗?"
):
self
.cancel_processing()
self
.destroy()
else
:
self
.destroy()
class
TurboProcessor:
def
__init__(
self
, api_key, input_file, output_dir, concurrency,
prompt_template, content_length, temperature, timeout, log_callback):
self
.api_key
=
api_key
self
.input_file
=
input_file
self
.output_file
=
f
"{output_dir}/result_{int(time.time())}.txt"
self
.concurrency
=
concurrency
self
.prompt_template
=
f
"{prompt_template} 内容:{{content}}"
self
.content_length
=
content_length
self
.temperature
=
temperature
self
.timeout
=
timeout
self
.log_callback
=
log_callback
self
.sem
=
asyncio.Semaphore(concurrency)
self
.cancelled
=
False
self
.tasks
=
[]
self
.chapters
=
[]
self
.results
=
[]
self
.total_chapters
=
0
self
.start_time
=
0.0
self
.success
=
0
self
.failed
=
0
def
cancel(
self
):
self
.cancelled
=
True
for
task
in
self
.tasks:
task.cancel()
def
_log(
self
, message:
str
):
if
self
.log_callback:
self
.log_callback(message)
async
def
process_chapter(
self
, chapter_info:
Tuple
[
int
,
str
])
-
>
str
:
if
self
.cancelled:
return
None
index, content
=
chapter_info
current
=
f
"{index}/{self.total_chapters}"
payload
=
{
"model"
:
"deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
,
"messages"
: [{
"role"
:
"user"
,
"content"
:
self
.prompt_template.
format
(content
=
content[:
self
.content_length])
}],
"temperature"
:
self
.temperature
}
async with
self
.sem:
for
attempt
in
range
(
5
+
1
):
try
:
async with aiohttp.ClientSession(
headers
=
{
"Authorization"
: f
"Bearer {self.api_key}"
},
timeout
=
aiohttp.ClientTimeout(
*
self
.timeout)
) as session:
if
self
.cancelled:
return
None
if
attempt
=
=
0
:
self
._log(f
"[章节 {current}] 开始处理"
)
else
:
self
._log(f
"[章节 {current}] 第{attempt}次重试"
)
start
=
time.monotonic()
async with session.post(
"https://api.siliconflow.cn/v1/chat/completions"
,
json
=
payload
) as resp:
if
resp.status
=
=
200
:
data
=
await resp.json()
elapsed
=
time.monotonic()
-
start
self
._log(f
"[章节 {current}] 成功(耗时 {elapsed:.1f}s)"
)
text
=
data[
'choices'
][
0
][
'message'
][
'content'
].strip()
return
re.sub(r
'\n{3,}'
,
'\n'
, text)
elif
resp.status
=
=
429
:
backoff
=
min
(
2
*
*
attempt,
30
)
self
._log(f
"[章节 {current}] 频率限制(等待{backoff}s)"
)
await asyncio.sleep(backoff)
else
:
self
._log(f
"[章节 {current}] HTTP错误 {resp.status}"
)
break
except
(asyncio.CancelledError, KeyboardInterrupt):
raise
except
Exception as e:
backoff
=
min
(
2
*
*
attempt,
30
)
self
._log(f
"[章节 {current}] 错误: {type(e).__name__}({attempt + 1}/5次)"
)
await asyncio.sleep(backoff)
self
._log(f
"[章节 {current}] 处理失败"
)
return
None
async
def
pipeline(
self
):
try
:
try
:
with
open
(
self
.input_file,
'r'
, encoding
=
'utf-8'
) as f:
text
=
f.read()
except
UnicodeDecodeError:
with
open
(
self
.input_file,
'r'
, encoding
=
'gbk'
) as f:
text
=
f.read()
raw_chapters
=
re.findall(r
'(第[^章]+章[\s\S]*?)(?=第[^章]+章|$)'
, text)
self
.total_chapters
=
len
(raw_chapters)
self
.chapters
=
list
(
enumerate
(raw_chapters, start
=
1
))
self
._log(f
"开始处理 {self.total_chapters} 个章节..."
)
self
.tasks
=
[asyncio.create_task(
self
.process_chapter((idx, ch)))
for
idx, ch
in
self
.chapters]
self
.results
=
await asyncio.gather(
*
self
.tasks, return_exceptions
=
True
)
with
open
(
self
.output_file,
'w'
, encoding
=
'utf-8'
) as f:
for
(idx, _), res
in
zip
(
self
.chapters,
self
.results):
if
res
and
not
isinstance
(res, Exception):
f.write(f
"=== 第{idx}章 ===\n{res}\n\n"
)
self
.success
+
=
1
else
:
f.write(f
"=== 第{idx}章处理失败 ===\n\n"
)
self
.failed
+
=
1
total_time
=
time.monotonic()
-
self
.start_time
self
._log(
"\n处理完成!\n"
f
"成功: {self.success}\n"
f
"失败: {self.failed}\n"
f
"总耗时: {total_time:.1f}s\n"
f
"输出文件: {self.output_file}"
)
except
asyncio.CancelledError:
self
._log(
"用户中止处理流程"
)
except
Exception as e:
self
._log(f
"处理出错: {str(e)}"
)
async
def
run(
self
):
self
.start_time
=
time.monotonic()
await
self
.pipeline()
if
__name__
=
=
"__main__"
:
window
=
ConfigWindow()
window.mainloop()