OddMinutes 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. oddminutes-0.1.0/.env.example +19 -0
  2. oddminutes-0.1.0/LICENSE +21 -0
  3. oddminutes-0.1.0/MANIFEST.in +10 -0
  4. oddminutes-0.1.0/OddMinutes/.env.example +19 -0
  5. oddminutes-0.1.0/OddMinutes/__init__.py +7 -0
  6. oddminutes-0.1.0/OddMinutes/launcher.py +446 -0
  7. oddminutes-0.1.0/OddMinutes/manage.py +22 -0
  8. oddminutes-0.1.0/OddMinutes.egg-info/PKG-INFO +630 -0
  9. oddminutes-0.1.0/OddMinutes.egg-info/SOURCES.txt +132 -0
  10. oddminutes-0.1.0/OddMinutes.egg-info/dependency_links.txt +1 -0
  11. oddminutes-0.1.0/OddMinutes.egg-info/entry_points.txt +2 -0
  12. oddminutes-0.1.0/OddMinutes.egg-info/requires.txt +28 -0
  13. oddminutes-0.1.0/OddMinutes.egg-info/top_level.txt +8 -0
  14. oddminutes-0.1.0/PKG-INFO +630 -0
  15. oddminutes-0.1.0/README.md +583 -0
  16. oddminutes-0.1.0/core/__init__.py +0 -0
  17. oddminutes-0.1.0/core/admin.py +3 -0
  18. oddminutes-0.1.0/core/apps.py +6 -0
  19. oddminutes-0.1.0/core/audio_utils.py +51 -0
  20. oddminutes-0.1.0/core/management/__init__.py +0 -0
  21. oddminutes-0.1.0/core/management/__pycache__/__init__.cpython-312.pyc +0 -0
  22. oddminutes-0.1.0/core/management/__pycache__/__init__.cpython-314.pyc +0 -0
  23. oddminutes-0.1.0/core/management/commands/__init__.py +0 -0
  24. oddminutes-0.1.0/core/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  25. oddminutes-0.1.0/core/management/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  26. oddminutes-0.1.0/core/management/commands/init_defaults.py +44 -0
  27. oddminutes-0.1.0/core/migrations/__init__.py +0 -0
  28. oddminutes-0.1.0/core/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  29. oddminutes-0.1.0/core/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  30. oddminutes-0.1.0/core/models.py +12 -0
  31. oddminutes-0.1.0/core/tests.py +3 -0
  32. oddminutes-0.1.0/core/urls.py +7 -0
  33. oddminutes-0.1.0/core/views.py +12 -0
  34. oddminutes-0.1.0/launcher.py +10 -0
  35. oddminutes-0.1.0/manage.py +22 -0
  36. oddminutes-0.1.0/meeting_minutes/__init__.py +0 -0
  37. oddminutes-0.1.0/meeting_minutes/asgi.py +25 -0
  38. oddminutes-0.1.0/meeting_minutes/settings.py +189 -0
  39. oddminutes-0.1.0/meeting_minutes/static/css/style.css +1021 -0
  40. oddminutes-0.1.0/meeting_minutes/static/favicon.ico +0 -0
  41. oddminutes-0.1.0/meeting_minutes/static/js/app.js +1917 -0
  42. oddminutes-0.1.0/meeting_minutes/templates/core/index.html +298 -0
  43. oddminutes-0.1.0/meeting_minutes/templates/core/meeting_detail.html +47 -0
  44. oddminutes-0.1.0/meeting_minutes/urls.py +23 -0
  45. oddminutes-0.1.0/meeting_minutes/wsgi.py +9 -0
  46. oddminutes-0.1.0/meetings/__init__.py +0 -0
  47. oddminutes-0.1.0/meetings/admin.py +3 -0
  48. oddminutes-0.1.0/meetings/apps.py +5 -0
  49. oddminutes-0.1.0/meetings/migrations/0001_initial.py +90 -0
  50. oddminutes-0.1.0/meetings/migrations/0002_transcriptsegment_words.py +18 -0
  51. oddminutes-0.1.0/meetings/migrations/0003_alter_meeting_status.py +18 -0
  52. oddminutes-0.1.0/meetings/migrations/__init__.py +0 -0
  53. oddminutes-0.1.0/meetings/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  54. oddminutes-0.1.0/meetings/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  55. oddminutes-0.1.0/meetings/migrations/__pycache__/0002_transcriptsegment_words.cpython-312.pyc +0 -0
  56. oddminutes-0.1.0/meetings/migrations/__pycache__/0002_transcriptsegment_words.cpython-314.pyc +0 -0
  57. oddminutes-0.1.0/meetings/migrations/__pycache__/0003_alter_meeting_status.cpython-312.pyc +0 -0
  58. oddminutes-0.1.0/meetings/migrations/__pycache__/0003_alter_meeting_status.cpython-314.pyc +0 -0
  59. oddminutes-0.1.0/meetings/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  60. oddminutes-0.1.0/meetings/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  61. oddminutes-0.1.0/meetings/models.py +95 -0
  62. oddminutes-0.1.0/meetings/serializers.py +67 -0
  63. oddminutes-0.1.0/meetings/tests.py +3 -0
  64. oddminutes-0.1.0/meetings/urls.py +11 -0
  65. oddminutes-0.1.0/meetings/views.py +239 -0
  66. oddminutes-0.1.0/pyproject.toml +77 -0
  67. oddminutes-0.1.0/recording/__init__.py +0 -0
  68. oddminutes-0.1.0/recording/admin.py +3 -0
  69. oddminutes-0.1.0/recording/apps.py +5 -0
  70. oddminutes-0.1.0/recording/audio_recorder.py +719 -0
  71. oddminutes-0.1.0/recording/consumers.py +86 -0
  72. oddminutes-0.1.0/recording/migrations/0001_initial.py +73 -0
  73. oddminutes-0.1.0/recording/migrations/__init__.py +0 -0
  74. oddminutes-0.1.0/recording/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  75. oddminutes-0.1.0/recording/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  76. oddminutes-0.1.0/recording/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  77. oddminutes-0.1.0/recording/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  78. oddminutes-0.1.0/recording/models.py +63 -0
  79. oddminutes-0.1.0/recording/routing.py +6 -0
  80. oddminutes-0.1.0/recording/tests.py +77 -0
  81. oddminutes-0.1.0/recording/urls.py +14 -0
  82. oddminutes-0.1.0/recording/views.py +231 -0
  83. oddminutes-0.1.0/requirements.txt +38 -0
  84. oddminutes-0.1.0/setup.cfg +4 -0
  85. oddminutes-0.1.0/static/css/style.css +1021 -0
  86. oddminutes-0.1.0/static/favicon.ico +0 -0
  87. oddminutes-0.1.0/static/js/app.js +1923 -0
  88. oddminutes-0.1.0/summary/__init__.py +0 -0
  89. oddminutes-0.1.0/summary/admin.py +3 -0
  90. oddminutes-0.1.0/summary/ai_processor.py +335 -0
  91. oddminutes-0.1.0/summary/apps.py +5 -0
  92. oddminutes-0.1.0/summary/migrations/0001_initial.py +62 -0
  93. oddminutes-0.1.0/summary/migrations/__init__.py +0 -0
  94. oddminutes-0.1.0/summary/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  95. oddminutes-0.1.0/summary/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  96. oddminutes-0.1.0/summary/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  97. oddminutes-0.1.0/summary/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  98. oddminutes-0.1.0/summary/models.py +43 -0
  99. oddminutes-0.1.0/summary/tests.py +3 -0
  100. oddminutes-0.1.0/summary/urls.py +12 -0
  101. oddminutes-0.1.0/summary/views.py +285 -0
  102. oddminutes-0.1.0/system_app/__init__.py +0 -0
  103. oddminutes-0.1.0/system_app/admin.py +3 -0
  104. oddminutes-0.1.0/system_app/apps.py +5 -0
  105. oddminutes-0.1.0/system_app/migrations/0001_initial.py +69 -0
  106. oddminutes-0.1.0/system_app/migrations/__init__.py +0 -0
  107. oddminutes-0.1.0/system_app/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  108. oddminutes-0.1.0/system_app/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  109. oddminutes-0.1.0/system_app/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  110. oddminutes-0.1.0/system_app/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  111. oddminutes-0.1.0/system_app/models.py +44 -0
  112. oddminutes-0.1.0/system_app/shortcuts.py +89 -0
  113. oddminutes-0.1.0/system_app/tests.py +3 -0
  114. oddminutes-0.1.0/system_app/tray_icon.py +168 -0
  115. oddminutes-0.1.0/system_app/urls.py +12 -0
  116. oddminutes-0.1.0/system_app/views.py +197 -0
  117. oddminutes-0.1.0/templates/core/index.html +298 -0
  118. oddminutes-0.1.0/templates/core/meeting_detail.html +47 -0
  119. oddminutes-0.1.0/transcription/__init__.py +0 -0
  120. oddminutes-0.1.0/transcription/admin.py +3 -0
  121. oddminutes-0.1.0/transcription/apps.py +5 -0
  122. oddminutes-0.1.0/transcription/consumers.py +79 -0
  123. oddminutes-0.1.0/transcription/migrations/0001_initial.py +54 -0
  124. oddminutes-0.1.0/transcription/migrations/__init__.py +0 -0
  125. oddminutes-0.1.0/transcription/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  126. oddminutes-0.1.0/transcription/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  127. oddminutes-0.1.0/transcription/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  128. oddminutes-0.1.0/transcription/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  129. oddminutes-0.1.0/transcription/models.py +47 -0
  130. oddminutes-0.1.0/transcription/oddasr_client.py +145 -0
  131. oddminutes-0.1.0/transcription/routing.py +6 -0
  132. oddminutes-0.1.0/transcription/tests.py +3 -0
  133. oddminutes-0.1.0/transcription/urls.py +11 -0
  134. oddminutes-0.1.0/transcription/views.py +356 -0
@@ -0,0 +1,19 @@
1
+ DJANGO_SECRET_KEY=your-secret-key-here
2
+ DEBUG=True
3
+ ALLOWED_HOSTS=localhost,127.0.0.1
4
+
5
+ # OddASR Configuration
6
+ ODDASR_HOST=127.0.0.1
7
+ ODDASR_PORT=12340
8
+
9
+ # AI Model Configuration
10
+ OLLAMA_HOST=http://localhost:11434
11
+
12
+ # Audio Configuration
13
+ AUDIO_SAMPLE_RATE=16000
14
+ AUDIO_CHANNELS=1
15
+ AUDIO_CHUNK_DURATION=5
16
+
17
+ # Meeting Settings
18
+ AUTO_CLEANUP_DAYS=7
19
+ MAX_AUDIO_FILE_SIZE=500
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OddMeta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ recursive-include static *
2
+ recursive-include templates *
3
+ include requirements.txt
4
+ include README.md
5
+ include LICENSE
6
+ include launcher.py
7
+ include manage.py
8
+ include .env.example
9
+ exclude .env
10
+ exclude db.sqlite3
@@ -0,0 +1,19 @@
1
+ DJANGO_SECRET_KEY=your-secret-key-here
2
+ DEBUG=True
3
+ ALLOWED_HOSTS=localhost,127.0.0.1
4
+
5
+ # OddASR Configuration
6
+ ODDASR_HOST=127.0.0.1
7
+ ODDASR_PORT=12340
8
+
9
+ # AI Model Configuration
10
+ OLLAMA_HOST=http://localhost:11434
11
+
12
+ # Audio Configuration
13
+ AUDIO_SAMPLE_RATE=16000
14
+ AUDIO_CHANNELS=1
15
+ AUDIO_CHUNK_DURATION=5
16
+
17
+ # Meeting Settings
18
+ AUTO_CLEANUP_DAYS=7
19
+ MAX_AUDIO_FILE_SIZE=500
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ OddMinutes - 小奥会议纪要
4
+ 智能会议录音与纪要系统
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,446 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import sys
4
+ import time
5
+ import subprocess
6
+ import webbrowser
7
+ import socket
8
+ import argparse
9
+ import importlib.util
10
+ import threading
11
+ import shutil
12
+ import appdirs
13
+
14
+
15
+ def setup_logging(project_dir):
16
+ log_dir = os.path.join(project_dir, 'logs')
17
+ os.makedirs(log_dir, exist_ok=True)
18
+ log_path = os.path.join(log_dir, 'launcher.log')
19
+
20
+ log_file = open(log_path, 'a', encoding='utf-8')
21
+ sys.stdout = log_file
22
+ sys.stderr = log_file
23
+
24
+ print(f'\n=== 启动器日志 {time.strftime("%Y-%m-%d %H:%M:%S")} ===')
25
+
26
+
27
+ def check_dependencies(project_dir):
28
+ critical_packages = {
29
+ 'django': 'django',
30
+ 'daphne': 'daphne',
31
+ 'channels': 'channels',
32
+ 'rest_framework': 'rest_framework',
33
+ 'dotenv': 'dotenv',
34
+ 'corsheaders': 'corsheaders',
35
+ 'pystray': 'pystray',
36
+ 'PIL': 'PIL',
37
+ 'requests': 'requests',
38
+ }
39
+
40
+ missing = []
41
+ for display_name, module_name in critical_packages.items():
42
+ if importlib.util.find_spec(module_name) is None:
43
+ missing.append(display_name)
44
+
45
+ if missing:
46
+ print(f'缺少以下依赖: {", ".join(missing)}')
47
+ print('正在自动安装依赖...')
48
+
49
+ requirements_path = os.path.join(project_dir, 'requirements.txt')
50
+
51
+ try:
52
+ result = subprocess.run(
53
+ [sys.executable, '-m', 'pip', 'install', '-r', requirements_path],
54
+ capture_output=True,
55
+ text=True,
56
+ encoding='utf-8',
57
+ creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
58
+ )
59
+
60
+ if result.returncode == 0:
61
+ print('依赖安装成功')
62
+ else:
63
+ print(f'依赖安装失败: {result.stderr}')
64
+ print('请手动运行: pip install -r requirements.txt')
65
+ sys.exit(1)
66
+
67
+ except Exception as e:
68
+ print(f'安装依赖时出错: {e}')
69
+ sys.exit(1)
70
+ else:
71
+ print('所有依赖已安装')
72
+
73
+
74
+ def check_port_in_use(port):
75
+ try:
76
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
77
+ return s.connect_ex(('127.0.0.1', port)) == 0
78
+ except Exception:
79
+ return False
80
+
81
+
82
+ def wait_for_server(url, timeout=30):
83
+ import requests
84
+ start_time = time.time()
85
+ while time.time() - start_time < timeout:
86
+ try:
87
+ requests.get(url, timeout=2)
88
+ return True
89
+ except Exception:
90
+ time.sleep(1)
91
+ return False
92
+
93
+
94
+ def start_server(project_dir, port):
95
+ os.chdir(project_dir)
96
+
97
+ env = os.environ.copy()
98
+ env['DJANGO_SETTINGS_MODULE'] = 'meeting_minutes.settings'
99
+
100
+ log_dir = os.path.join(project_dir, 'logs')
101
+ os.makedirs(log_dir, exist_ok=True)
102
+ log_path = os.path.join(log_dir, 'daphne.log')
103
+
104
+ print('初始化数据库...')
105
+ try:
106
+ migrate_result = subprocess.run(
107
+ [sys.executable, 'manage.py', 'migrate'],
108
+ env=env,
109
+ capture_output=True,
110
+ text=True,
111
+ encoding='utf-8',
112
+ creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
113
+ )
114
+ if migrate_result.returncode == 0:
115
+ print('数据库初始化成功')
116
+ else:
117
+ print(f'数据库初始化失败: {migrate_result.stderr}')
118
+ except Exception as e:
119
+ print(f'数据库初始化异常: {e}')
120
+
121
+ server_cmd = [
122
+ sys.executable, '-m', 'daphne',
123
+ 'meeting_minutes.asgi:application',
124
+ '--port', str(port),
125
+ '--bind', '127.0.0.1'
126
+ ]
127
+
128
+ log_file = open(log_path, 'a', encoding='utf-8')
129
+
130
+ process = subprocess.Popen(
131
+ server_cmd,
132
+ env=env,
133
+ stdout=log_file,
134
+ stderr=log_file,
135
+ creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
136
+ )
137
+ return process
138
+
139
+
140
+ def get_package_dir():
141
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
142
+
143
+
144
+ def bootstrap_user_data_dir():
145
+ package_dir = get_package_dir()
146
+ user_data_dir = appdirs.user_data_dir('OddMinutes', 'OddMeta')
147
+
148
+ os.makedirs(user_data_dir, exist_ok=True)
149
+
150
+ required_files = ['manage.py', '.env.example']
151
+ required_dirs = ['meeting_minutes', 'templates', 'static', 'core', 'meetings', 'recording', 'transcription', 'summary', 'system_app']
152
+
153
+ for filename in required_files:
154
+ src = os.path.join(package_dir, filename)
155
+ dst = os.path.join(user_data_dir, filename)
156
+ if os.path.exists(src) and not os.path.exists(dst):
157
+ shutil.copy2(src, dst)
158
+
159
+ for dirname in required_dirs:
160
+ src = os.path.join(package_dir, dirname)
161
+ dst = os.path.join(user_data_dir, dirname)
162
+ if os.path.isdir(src) and not os.path.exists(dst):
163
+ shutil.copytree(src, dst, dirs_exist_ok=True)
164
+ elif os.path.isdir(src) and os.path.isdir(dst):
165
+ for item in os.listdir(src):
166
+ s = os.path.join(src, item)
167
+ d = os.path.join(dst, item)
168
+ if os.path.isdir(s) and not os.path.exists(d):
169
+ shutil.copytree(s, d, dirs_exist_ok=True)
170
+ elif os.path.isfile(s) and not os.path.exists(d):
171
+ shutil.copy2(s, d)
172
+
173
+ env_path = os.path.join(user_data_dir, '.env')
174
+ env_example_path = os.path.join(user_data_dir, '.env.example')
175
+ if not os.path.exists(env_path) and os.path.exists(env_example_path):
176
+ shutil.copy2(env_example_path, env_path)
177
+
178
+ media_dir = os.path.join(user_data_dir, 'media')
179
+ os.makedirs(media_dir, exist_ok=True)
180
+
181
+ return user_data_dir
182
+
183
+
184
+ def find_project_dir():
185
+ candidates = []
186
+
187
+ candidates.append(os.getcwd())
188
+
189
+ candidates.append(os.environ.get('ODDMINUTES_PROJECT_DIR'))
190
+
191
+ script_dir = get_package_dir()
192
+ candidates.append(script_dir)
193
+
194
+ for candidate in candidates:
195
+ if candidate and os.path.isfile(os.path.join(candidate, 'manage.py')):
196
+ return candidate
197
+
198
+ print(f'未找到项目目录,正在从包安装位置引导...')
199
+ user_data_dir = bootstrap_user_data_dir()
200
+ print(f'项目目录已引导至: {user_data_dir}')
201
+ return user_data_dir
202
+
203
+
204
+ def main():
205
+ parser = argparse.ArgumentParser(description='小奥会议纪要启动器')
206
+ parser.add_argument('--port', type=int, default=9011, help='服务端口')
207
+ parser.add_argument('--no-browser', action='store_true', help='不自动打开浏览器')
208
+ parser.add_argument('--debug', action='store_true', help='显示控制台窗口')
209
+ parser.add_argument('--project-dir', type=str, help='项目目录')
210
+ args = parser.parse_args()
211
+
212
+ if args.project_dir:
213
+ project_dir = args.project_dir
214
+ else:
215
+ project_dir = find_project_dir()
216
+
217
+ url = f'http://127.0.0.1:{args.port}'
218
+
219
+ if not args.debug:
220
+ setup_logging(project_dir)
221
+
222
+ check_dependencies(project_dir)
223
+
224
+ if check_port_in_use(args.port):
225
+ print(f'端口 {args.port} 已被占用,直接打开浏览器')
226
+ if not args.no_browser:
227
+ webbrowser.open(url)
228
+ return
229
+
230
+ print(f'启动小奥会议纪要...')
231
+ print(f'项目目录: {project_dir}')
232
+
233
+ process = start_server(project_dir, args.port)
234
+
235
+ print(f'等待服务器启动...')
236
+ if wait_for_server(url):
237
+ print(f'服务器已启动: {url}')
238
+ if not args.no_browser:
239
+ webbrowser.open(url)
240
+ print('服务已启动,日志文件: logs/launcher.log')
241
+
242
+ sys.path.insert(0, project_dir)
243
+ try:
244
+ from system_app.tray_icon import get_tray_manager
245
+ import requests
246
+
247
+ tray_manager = get_tray_manager()
248
+
249
+ recording_status = {'recording': False, 'paused': False, 'meeting_id': None}
250
+
251
+ def fetch_recording_status():
252
+ try:
253
+ status_url = f'{url}/api/recording/status/'
254
+ resp = requests.get(status_url, timeout=5)
255
+ data = resp.json()
256
+ is_recording = data.get('recording', False)
257
+ recording_status['recording'] = is_recording
258
+ recording_status['meeting_id'] = data.get('meeting_id')
259
+ if is_recording:
260
+ tray_manager.update_tooltip('正在录音...')
261
+ else:
262
+ tray_manager.update_tooltip('小奥会议纪要')
263
+ tray_manager.update_recording_state(is_recording, recording_status.get('paused', False))
264
+ except Exception as e:
265
+ print(f'获取录音状态失败: {e}')
266
+
267
+ fetch_recording_status()
268
+
269
+ def status_polling_thread():
270
+ while True:
271
+ try:
272
+ fetch_recording_status()
273
+ time.sleep(3)
274
+ except Exception as e:
275
+ print(f'状态轮询异常: {e}')
276
+ time.sleep(5)
277
+
278
+ polling_thread = threading.Thread(target=status_polling_thread, daemon=True)
279
+ polling_thread.start()
280
+
281
+ def on_exit():
282
+ if recording_status['recording']:
283
+ stop_recording(url, recording_status)
284
+ process.terminate()
285
+ print('服务已停止')
286
+ sys.exit(0)
287
+
288
+ def on_show_window():
289
+ webbrowser.open(url)
290
+
291
+ def on_new_meeting():
292
+ try:
293
+ create_meeting_url = f'{url}/api/meetings/'
294
+ create_data = {
295
+ 'title': f'新会议 {time.strftime("%Y-%m-%d %H:%M:%S")}',
296
+ }
297
+ resp = requests.post(create_meeting_url, json=create_data, timeout=10)
298
+ result = resp.json()
299
+
300
+ if resp.status_code == 409:
301
+ error_msg = result.get('error', '创建会议失败')
302
+ print(f'创建会议失败: {error_msg}')
303
+ tray_manager.update_tooltip(error_msg)
304
+ return
305
+
306
+ meeting_id = result.get('id')
307
+ if meeting_id:
308
+ print(f'新会议已创建: {meeting_id}')
309
+ recording_status['meeting_id'] = meeting_id
310
+ tray_manager.update_tooltip('新会议已创建')
311
+ else:
312
+ print(f'创建会议失败: {result}')
313
+ except Exception as e:
314
+ print(f'创建新会议失败: {e}')
315
+
316
+ def on_start_recording():
317
+ try:
318
+ status_url = f'{url}/api/recording/status/'
319
+ status_resp = requests.get(status_url, timeout=5)
320
+ status_data = status_resp.json()
321
+ if status_data.get('recording'):
322
+ print('已有会议正在录音中,无法启动新录音')
323
+ tray_manager.update_tooltip('已有会议正在录音中')
324
+ return
325
+
326
+ meeting_id = recording_status.get('meeting_id')
327
+
328
+ if not meeting_id:
329
+ create_meeting_url = f'{url}/api/meetings/'
330
+ create_data = {
331
+ 'title': f'快速录音 {time.strftime("%Y-%m-%d %H:%M:%S")}',
332
+ }
333
+ resp = requests.post(create_meeting_url, json=create_data, timeout=10)
334
+ result = resp.json()
335
+ meeting_id = result.get('id')
336
+ if meeting_id:
337
+ recording_status['meeting_id'] = meeting_id
338
+ else:
339
+ print('创建会议失败')
340
+ return
341
+
342
+ start_recording_url = f'{url}/api/recording/start/'
343
+ start_data = {
344
+ 'meeting_id': meeting_id,
345
+ }
346
+ start_resp = requests.post(start_recording_url, json=start_data, timeout=10)
347
+ start_result = start_resp.json()
348
+ if start_resp.ok and start_result.get('status') == 'recording':
349
+ recording_status['recording'] = True
350
+ recording_status['paused'] = False
351
+ tray_manager.update_tooltip('正在录音...')
352
+ tray_manager.update_recording_state(True, False)
353
+ print('录音已开始')
354
+ else:
355
+ print(f'启动录音失败: {start_result}')
356
+ if 'error' in start_result:
357
+ tray_manager.update_tooltip(start_result['error'])
358
+ except Exception as e:
359
+ print(f'开始录音失败: {e}')
360
+
361
+ def on_stop_recording():
362
+ stop_recording(url, recording_status)
363
+ tray_manager.update_recording_state(False, False)
364
+
365
+ def on_pause_recording():
366
+ if recording_status['meeting_id']:
367
+ try:
368
+ pause_url = f'{url}/api/recording/pause/'
369
+ pause_data = {
370
+ 'meeting_id': recording_status['meeting_id'],
371
+ }
372
+ resp = requests.post(pause_url, json=pause_data, timeout=10)
373
+ result = resp.json()
374
+ if resp.ok:
375
+ recording_status['paused'] = True
376
+ tray_manager.update_tooltip('录音已暂停')
377
+ tray_manager.update_recording_state(True, True)
378
+ print('录音已暂停')
379
+ else:
380
+ print(f'暂停录音失败: {result}')
381
+ except Exception as e:
382
+ print(f'暂停录音失败: {e}')
383
+
384
+ def on_resume_recording():
385
+ if recording_status['meeting_id']:
386
+ try:
387
+ resume_url = f'{url}/api/recording/resume/'
388
+ resume_data = {
389
+ 'meeting_id': recording_status['meeting_id'],
390
+ }
391
+ resp = requests.post(resume_url, json=resume_data, timeout=10)
392
+ result = resp.json()
393
+ if resp.ok:
394
+ recording_status['paused'] = False
395
+ tray_manager.update_tooltip('正在录音...')
396
+ tray_manager.update_recording_state(True, False)
397
+ print('录音已继续')
398
+ else:
399
+ print(f'继续录音失败: {result}')
400
+ except Exception as e:
401
+ print(f'继续录音失败: {e}')
402
+
403
+ def stop_recording(base_url, status):
404
+ if status['meeting_id']:
405
+ try:
406
+ stop_url = f'{base_url}/api/recording/stop/'
407
+ stop_data = {
408
+ 'meeting_id': status['meeting_id'],
409
+ }
410
+ resp = requests.post(stop_url, json=stop_data, timeout=10)
411
+ result = resp.json()
412
+ print(f'录音已停止: {result}')
413
+ except Exception as e:
414
+ print(f'停止录音失败: {e}')
415
+
416
+ status['recording'] = False
417
+ status['paused'] = False
418
+ status['meeting_id'] = None
419
+ tray_manager.update_tooltip('小奥会议纪要')
420
+
421
+ tray_manager.set_callbacks(
422
+ on_new_meeting=on_new_meeting,
423
+ on_start_recording=on_start_recording,
424
+ on_stop_recording=on_stop_recording,
425
+ on_pause_recording=on_pause_recording,
426
+ on_resume_recording=on_resume_recording,
427
+ on_show_window=on_show_window,
428
+ on_exit=on_exit
429
+ )
430
+ tray_manager.start()
431
+ print('系统托盘图标已启动')
432
+ except Exception as e:
433
+ print(f'启动系统托盘图标失败: {e}')
434
+
435
+ try:
436
+ process.wait()
437
+ except KeyboardInterrupt:
438
+ process.terminate()
439
+ print('服务已停止')
440
+ else:
441
+ print('服务器启动超时')
442
+ process.terminate()
443
+
444
+
445
+ if __name__ == '__main__':
446
+ main()
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python
2
+ """Django's command-line utility for administrative tasks."""
3
+ import os
4
+ import sys
5
+
6
+
7
+ def main():
8
+ """Run administrative tasks."""
9
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'meeting_minutes.settings')
10
+ try:
11
+ from django.core.management import execute_from_command_line
12
+ except ImportError as exc:
13
+ raise ImportError(
14
+ "Couldn't import Django. Are you sure it's installed and "
15
+ "available on your PYTHONPATH environment variable? Did you "
16
+ "forget to activate a virtual environment?"
17
+ ) from exc
18
+ execute_from_command_line(sys.argv)
19
+
20
+
21
+ if __name__ == '__main__':
22
+ main()