gnilets commited on
Commit
6105f78
1 Parent(s): e4bfa6c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +287 -0
app.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from base64 import b64decode
2
+ from contextlib import asynccontextmanager
3
+ from datetime import datetime
4
+ from io import BytesIO
5
+ from json import loads
6
+ from logging import DEBUG, Formatter, INFO, StreamHandler, WARNING, getLogger
7
+ from pathlib import Path
8
+ from typing import AsyncGenerator, BinaryIO
9
+
10
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
11
+ from fastapi import FastAPI, File, Form, Request, UploadFile
12
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
13
+ from httpx import AsyncClient
14
+ from patchright.async_api import Page, async_playwright
15
+ from prlps_fakeua import UserAgent
16
+ from starlette.responses import Response
17
+
18
+ ua = UserAgent()
19
+
20
+ AUTH_TOKEN = ''
21
+
22
+ screenshot_path = Path(__file__).parent / 'screenshot.jpeg'
23
+ token_path = Path(__file__).parent / 'token.json'
24
+ BROWSER_TIMEOUT_SECONDS = 15
25
+
26
+
27
+ logger = getLogger('RHYMES_AI_API')
28
+ logger.setLevel(DEBUG)
29
+ handler = StreamHandler()
30
+ handler.setLevel(INFO)
31
+ formatter = Formatter('%(asctime)s | %(levelname)s : %(message)s', datefmt='%d.%m.%Y %H:%M:%S')
32
+ handler.setFormatter(formatter)
33
+ logger.addHandler(handler)
34
+ getLogger('httpx').setLevel(WARNING)
35
+
36
+ logger.info('инициализация приложения...')
37
+
38
+
39
+ async def make_screenshot(page: Page):
40
+ await page.screenshot(type='jpeg', path=screenshot_path.resolve().as_posix(), quality=85, full_page=True)
41
+ logger.info('скриншот создан')
42
+
43
+
44
+ async def refresh_token():
45
+ max_timeout = BROWSER_TIMEOUT_SECONDS * 1000
46
+ user_token = None
47
+ async with async_playwright() as playwright:
48
+ logger.info('запуск браузера')
49
+ browser = await playwright.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled'])
50
+ context = await browser.new_context(
51
+ color_scheme='dark',
52
+ ignore_https_errors=True,
53
+ locale='en-US',
54
+ user_agent=ua.random,
55
+ no_viewport=True,
56
+ )
57
+ context.set_default_timeout(max_timeout)
58
+ context.set_default_navigation_timeout(max_timeout)
59
+ page = await context.new_page()
60
+ page.set_default_timeout(max_timeout)
61
+ page.set_default_navigation_timeout(max_timeout)
62
+ try:
63
+ logger.info('открытие страницы авторизации')
64
+ await page.goto('https://rhymes.ai/', wait_until='networkidle')
65
+ await make_screenshot(page)
66
+ local_storage = await page.evaluate('() => {return JSON.stringify(localStorage);}')
67
+ user_token = loads(local_storage).get('y-rhymes-user-token', '').split('|-door-|')[-1]
68
+ await make_screenshot(page)
69
+ finally:
70
+ await page.close()
71
+ await context.close()
72
+ await browser.close()
73
+ logger.info('работа браузера завершена')
74
+ return user_token
75
+
76
+
77
+ async def get_token():
78
+ global AUTH_TOKEN
79
+ token = await refresh_token()
80
+ if token:
81
+ AUTH_TOKEN = token
82
+ logger.info('токен получен')
83
+ else:
84
+ logger.error('токен не был получен')
85
+
86
+
87
+ async def periodic_get_token(scheduler: AsyncIOScheduler):
88
+ logger.info('запуск задачи периодического обновления токена')
89
+ scheduler.add_job(
90
+ get_token,
91
+ trigger='interval',
92
+ hours=24,
93
+ next_run_time=datetime.now(),
94
+ misfire_grace_time=3600
95
+ )
96
+
97
+
98
+ async def process_requests(api_key: str, image_file: BinaryIO, content: str) -> str | None:
99
+ async with AsyncClient() as client:
100
+ models_response = await client.get(
101
+ 'https://api.rhymes.ai/web/v1/models',
102
+ headers={
103
+ 'accept': '*/*',
104
+ 'authorization': api_key,
105
+ 'content-type': 'application/json'
106
+ }
107
+ )
108
+ if models_response.status_code != 200:
109
+ return None
110
+
111
+ models_data = models_response.json()
112
+ model_id = models_data.get('data', [{}])[0].get('id')
113
+ if not model_id:
114
+ return None
115
+ conversation_response = await client.post(
116
+ 'https://api.rhymes.ai/web/v1/conversation/create',
117
+ headers={
118
+ 'accept': '*/*',
119
+ 'authorization': api_key,
120
+ 'content-type': 'application/json'
121
+ },
122
+ json={
123
+ 'model': model_id,
124
+ 'language': 'en'
125
+ }
126
+ )
127
+ if conversation_response.status_code != 200:
128
+ return None
129
+
130
+ conversation_data = conversation_response.json()
131
+ conversation_id = conversation_data.get('data', {}).get('conversationId')
132
+ if not conversation_id:
133
+ return None
134
+
135
+ upload_response = await client.post(
136
+ 'https://api.rhymes.ai/web/v1/upload/files',
137
+ headers={
138
+ 'accept': '*/*',
139
+ 'authorization': api_key
140
+ },
141
+ files={
142
+ 'file': ('image.jpeg', image_file, 'image/jpeg'),
143
+ 'la': (None, '1')
144
+ }
145
+ )
146
+
147
+ if upload_response.status_code != 200:
148
+ return None
149
+
150
+ upload_data = upload_response.json()
151
+ file_url = upload_data.get('data', [{}])[0].get('fileUrl')
152
+ if not file_url:
153
+ return None
154
+
155
+ chat_response = await client.post(
156
+ 'https://api.rhymes.ai/web/v1/stream/chat',
157
+ headers={
158
+ 'accept': 'text/event-stream',
159
+ 'authorization': api_key,
160
+ 'content-type': 'application/json'
161
+ },
162
+ json={
163
+ 'content': content,
164
+ 'conversationId': conversation_id,
165
+ 'regenerate': 0,
166
+ 'parent_message_id': None,
167
+ 'model': model_id,
168
+ 'imageList': [file_url],
169
+ 'videoImageUrl': {},
170
+ 'fullImagePath': '',
171
+ 'split_image': False
172
+ }
173
+ )
174
+
175
+ if chat_response.status_code != 200:
176
+ return None
177
+
178
+ async for line in chat_response.aiter_lines():
179
+ if line.startswith('data:'):
180
+ message_data = loads(line[5:])
181
+ if message_data.get('lastOne'):
182
+ result: str = message_data.get('content')
183
+ return result
184
+ return None
185
+
186
+
187
+ @asynccontextmanager
188
+ async def app_lifespan(_) -> AsyncGenerator:
189
+ logger.info('запуск приложения')
190
+ scheduler = AsyncIOScheduler()
191
+ await periodic_get_token(scheduler)
192
+ try:
193
+ logger.info('запуск переодических задач')
194
+ scheduler.start()
195
+ logger.info('старт API')
196
+ yield
197
+ finally:
198
+ scheduler.shutdown()
199
+ logger.info('приложение завершено')
200
+
201
+
202
+ app = FastAPI(lifespan=app_lifespan, title='RHYMES_AI_API')
203
+
204
+ banned_endpoints = [
205
+ '/openapi.json',
206
+ '/docs',
207
+ '/docs/oauth2-redirect',
208
+ 'swagger_ui_redirect',
209
+ '/redoc',
210
+ ]
211
+
212
+
213
+ @app.middleware('http')
214
+ async def block_banned_endpoints(request: Request, call_next):
215
+ logger.debug(f'получен запрос: {request.url.path}')
216
+ if request.url.path in banned_endpoints:
217
+ logger.warning(f'запрещенный endpoint: {request.url.path}')
218
+ return Response(status_code=403)
219
+ response = await call_next(request)
220
+ return response
221
+
222
+
223
+ @app.post('/describe/')
224
+ async def describe(question: str = Form(...), file: UploadFile = File(...)):
225
+ logger.info('запрос `describe`')
226
+ caption = await process_requests(AUTH_TOKEN, file.file, question)
227
+ return JSONResponse({'caption': caption}, status_code=200, media_type='application/json')
228
+
229
+
230
+ @app.post('/v1/describe')
231
+ async def describe_v1(request: Request):
232
+ logger.info('запрос `describe_v1`')
233
+ body = await request.json()
234
+ content_text = ''
235
+ image_data = None
236
+
237
+ messages = body.get('messages', [])
238
+ for message in messages:
239
+ role = message.get('role')
240
+ content = message.get('content')
241
+
242
+ if role in ['system', 'user']:
243
+ if isinstance(content, str):
244
+ content_text += content + ' '
245
+ elif isinstance(content, list):
246
+ for item in content:
247
+ if item.get('type') == 'text':
248
+ content_text += item.get('text', '') + ' '
249
+ elif item.get('type') == 'image_url':
250
+ image_url = item.get('image_url', {})
251
+ url = image_url.get('url')
252
+ if url and url.startswith('data:image/'):
253
+ base64_encoded_data = url.split('base64,', 1)[1]
254
+ image_data = b64decode(base64_encoded_data)
255
+
256
+ if not content_text.strip() or image_data is None:
257
+ return JSONResponse({'caption': 'изображение должно быть передано как строка base64 `data:image/jpeg;base64,{base64_img}` а также текст'}, status_code=400)
258
+ try:
259
+ caption = await process_requests(AUTH_TOKEN, BytesIO(image_data), content_text.strip())
260
+ return JSONResponse({'caption': caption}, status_code=200)
261
+ except Exception as e:
262
+ return JSONResponse({'caption': str(e)}, status_code=500)
263
+
264
+
265
+ @app.get('/')
266
+ async def root():
267
+ return HTMLResponse('ну пролапс, ну и что', status_code=200)
268
+
269
+
270
+ @app.get('/api/update_token')
271
+ async def update_token():
272
+ logger.info('запрос `update_token`')
273
+ task = get_token()
274
+ return JSONResponse({'status': 'обновление токена запущено'}, status_code=200, media_type='application/json')
275
+
276
+
277
+ @app.get('/api/show_last_screen')
278
+ async def show_last_screen():
279
+ logger.info('запрос `show_last_screen`')
280
+ return FileResponse(screenshot_path.resolve().as_posix(), media_type='image/jpeg', status_code=200)
281
+
282
+
283
+ if __name__ == '__main__':
284
+ from uvicorn import run as uvicorn_run
285
+
286
+ logger.info('запуск сервера uvicorn')
287
+ uvicorn_run(app, host='0.0.0.0', port=7860)