feat: im1

This commit is contained in:
KofK 2025-02-10 15:53:52 +03:00
parent 435b7f3fca
commit 0c81c88228
66 changed files with 4723 additions and 16262 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/uploads
/__pycache__
/myenv
imageboard.db

3
README.md Normal file
View File

@ -0,0 +1,3 @@
docker run -p 6333:6333 -p 6334:6334 -d qdrant/qdrant
source myenv/bin/activate
uvicorn main:app --reload

View File

@ -1,2 +0,0 @@
DATABASE_URL=
SERVICE_TOKEN=

View File

@ -1,25 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

56
back/.gitignore vendored
View File

@ -1,56 +0,0 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -1,23 +0,0 @@
# Use the official Node.js image
FROM node:18
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the application
RUN npm run build
# Expose the port the app runs on
EXPOSE 3000
# Start the application
CMD ["npm", "run", "start:dev"]

View File

@ -1,73 +0,0 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@ -1,34 +0,0 @@
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg16
container_name: postgres
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- mynetwork
app:
build: .
container_name: app
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgresql://user:password@postgres:5432/mydatabase"
depends_on:
- postgres
networks:
- mynetwork
networks:
mynetwork:
driver: bridge
volumes:
postgres_data:

View File

@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

9919
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
{
"name": "vdkch",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@langchain/community": "^0.2.28",
"@langchain/ollama": "^0.0.4",
"@nestjs/common": "^10.4.1",
"@nestjs/core": "^10.4.1",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.1",
"@prisma/client": "^5.18.0",
"dotenv": "^16.4.5",
"langchain": "^0.2.16",
"multer": "^1.4.5-lts.1",
"passport": "^0.7.0",
"passport-http": "^0.3.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.16.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"prisma": "^5.18.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1,20 +0,0 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pgvector(map: "vector")]
}
model Post {
id Int @id @default(autoincrement())
text String?
media String? // URL или путь к файлу
mediaType String? // "image", "video", "drawing", "code"
code String? // Код, который нужно запустить
embedding Float[] //Unsupported("vector")?
}

View File

@ -1,19 +0,0 @@
import { Module, Injectable, Logger } from '@nestjs/common';
import { resolve } from 'path';
import * as dotenv from 'dotenv';
export type PossibleModels =
| 'text-davinci-002-render'
| 'text-davinci-003'
| 'gpt-3.5-turbo';
@Injectable()
export class AppConfig {
public readonly serviceToken: string = process.env.SERVICE_TOKEN || '';
}
@Module({
providers: [AppConfig],
exports: [AppConfig],
})
export class AppConfigModule {}

View File

@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
import { PrismaService } from './prisma.service';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [AuthModule, PostsModule],
providers: [PrismaService],
})
export class AppModule {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -1,14 +0,0 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { AppConfig } from "src/app.config";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private appConfig: AppConfig,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
return authHeader == this.appConfig.serviceToken;
}
}

View File

@ -1,10 +0,0 @@
import { Module } from "@nestjs/common";
import { AppConfigModule } from "src/app.config";
import { AuthGuard } from "./auth.guard";
@Module({
imports: [AppConfigModule],
providers: [AuthGuard],
exports: [AuthGuard],
})
export class AuthModule {}

View File

@ -1,11 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: '*', // Замените на домен вашего другого проекта
});
await app.listen(3000);
}
bootstrap();

View File

@ -1,8 +0,0 @@
export interface Post {
id?: number;
text: string;
imageUrl?: string;
videoUrl?: string;
drawing?: string;
code?: string;
}

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PostsController } from './posts.controller';
describe('PostsController', () => {
let controller: PostsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PostsController],
}).compile();
controller = module.get<PostsController>(PostsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -1,19 +0,0 @@
import { Controller, Post, Body, Get, UseGuards } from '@nestjs/common';
import { PostsService } from './posts.service';
import { AuthGuard } from "../auth/auth.guard";
@UseGuards(AuthGuard)
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post()
async create(@Body() createPostDto: { text?: string; media?: string; mediaType?: string; code?: string; embedding?: number[] }) {
return this.postsService.create(createPostDto);
}
@Get()
async findAll() {
return this.postsService.findAll();
}
}

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { PrismaService } from '../prisma.service';
import { AppConfigModule } from 'src/app.config';
@Module({
imports: [AppConfigModule],
providers: [PostsService, PrismaService],
controllers: [PostsController],
})
export class PostsModule {}

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PostsService } from './posts.service';
describe('PostsService', () => {
let service: PostsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PostsService],
}).compile();
service = module.get<PostsService>(PostsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,36 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { OllamaEmbeddings } from '@langchain/ollama';
const embeddings = new OllamaEmbeddings({
model: 'llama2',
baseUrl: 'http://host.docker.internal:11434',
});
@Injectable()
export class PostsService {
constructor(private readonly prisma: PrismaService) {}
async create(data: { text?: string; media?: string; mediaType?: string; code?: string; embedding?: number[] }) {
const splitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
chunkSize: 500,
chunkOverlap: 0,
});
const output = await splitter.createDocuments([data.text]);
console.log(output)
const vectorstore = await MemoryVectorStore.fromDocuments(output, embeddings);
console.log(vectorstore);
data.embedding ??= vectorstore.memoryVectors[0].embedding
return this.prisma.post.create({
data,
});
}
async findAll() {
return this.prisma.post.findMany();
}
}

View File

@ -1,10 +0,0 @@
import 'dotenv/config';
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
super();
}
}

View File

@ -1,24 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -1,23 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: '3.8'
services:
qdrant:
image: qdrant/qdrant
container_name: qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stopped
volumes:
qdrant_data:

View File

@ -1 +0,0 @@
REACT_APP_SERVICE_TOKEN=1234

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,13 +0,0 @@
// src/App.js
import React from 'react';
import Posts from './Posts';
function App() {
return (
<div className="App">
<Posts />
</div>
);
}
export default App;

View File

@ -1,75 +0,0 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import ReactMarkdown from 'react-markdown';
import MarkdownEditor from 'react-markdown-editor-lite';
import 'react-markdown-editor-lite/lib/index.css';
import MarkdownIt from 'markdown-it';
// Инициализация MarkdownIt
const mdParser = new MarkdownIt();
const Posts = () => {
const [posts, setPosts] = useState([]);
const [text, setText] = useState('');
const axiosInstance = axios.create({
baseURL: 'http://localhost:3000',
headers: {
Authorization: process.env.REACT_APP_SERVICE_TOKEN,
},
});
useEffect(() => {
fetchPosts();
}, []);
const fetchPosts = async () => {
try {
const response = await axiosInstance.get('/posts');
setPosts(response.data);
} catch (error) {
console.error('Error fetching posts:', error);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const newPost = { text };
await axiosInstance.post('/posts', newPost);
fetchPosts(); // Обновляем список постов после создания нового
setText('');
} catch (error) {
console.error('Error creating post:', error);
}
};
const handleEditorChange = ({ text }) => {
setText(text);
};
return (
<div>
<h1>Posts</h1>
<form onSubmit={handleSubmit}>
<MarkdownEditor
value={text}
style={{ height: '300px' }}
renderHTML={(text) => mdParser.render(text)}
onChange={handleEditorChange}
/>
<button type="submit">Create Post</button>
</form>
<h2>All Posts</h2>
<ul>
{posts.map((post, index) => (
<li key={index}>
<ReactMarkdown>{post.text}</ReactMarkdown>
</li>
))}
</ul>
</div>
);
};
export default Posts;

View File

@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,15 @@
{
"name": "front",
"name": "futa-clone",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.4",
"markdown-it": "^14.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-markdown-editor-lite": "^1.3.4",
"cra-template": "1.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.5",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"vis-network": "^9.1.9",
"web-vitals": "^4.2.4"
},
"scripts": {
"start": "react-scripts start",

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

162
futa-clone/src/App.css Normal file
View File

@ -0,0 +1,162 @@
/* App.css */
/*#network {
width: 100%;
height: 600px;
border: 1px solid #800;
background: white;
}
.hidden { display: none; }
#greeting {
text-align: center;
padding: 20px;
}
.graph-container {
margin-top: 20px;
}
body {
background-color: #F0E0D6;
font-family: Arial, sans-serif;
margin: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.post-form {
background: #FFF;
border: 1px solid #800;
padding: 15px;
margin-bottom: 20px;
}
.post {
background: #FFF;
border: 1px solid #800;
padding: 15px;
margin-bottom: 10px;
}
.post img {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
}
input, textarea, button {
margin: 5px;
padding: 5px;
}
.preview-image {
max-width: 150px;
margin: 10px 0;
}*/
/* App.css */
#network {
width: 100%;
height: 600px;
border: 1px solid rgba(54, 102, 153, 0.3); /* Приглушенный синий с прозрачностью */
background: rgba(245, 249, 252, 0.9); /* Светлый голубовато-белый фон */
backdrop-filter: blur(2px);
border-radius: 4px;
}
.hidden { display: none; }
#greeting {
text-align: center;
padding: 20px;
color: #2c3e50; /* Темно-синий для контраста */
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5);
}
.graph-container {
margin-top: 20px;
background: rgba(245, 249, 252, 0.8);
border-radius: 8px;
padding: 15px;
}
body {
background-color: #f5f9fc; /* Основной фоновый цвет - холодный бело-голубой */
font-family: 'Segoe UI', Arial, sans-serif;
margin: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.post-form {
background: rgba(255, 255, 255, 0.95); /* Полупрозрачный белый */
border: 1px solid rgba(54, 102, 153, 0.2);
padding: 15px;
margin-bottom: 20px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.post {
background: rgba(255, 255, 255, 0.95); /* Полупрозрачный белый */
border: 1px solid rgba(54, 102, 153, 0.15);
padding: 15px;
margin-bottom: 10px;
border-radius: 6px;
backdrop-filter: blur(3px);
transition: transform 0.2s ease;
}
.post:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.post img {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
border-radius: 4px;
border: 1px solid rgba(54, 102, 153, 0.1);
}
input, textarea {
margin: 5px;
padding: 8px;
border: 1px solid rgba(54, 102, 153, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
input:focus, textarea:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(54, 102, 153, 0.2);
}
button {
margin: 5px;
padding: 8px 16px;
background: rgba(54, 102, 153, 0.85); /* Полупрозрачный синий */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
background: rgba(54, 102, 153, 1);
transform: scale(1.02);
}
.preview-image {
max-width: 150px;
margin: 10px 0;
border-radius: 4px;
opacity: 0.9;
}

25
futa-clone/src/App.js Normal file
View File

@ -0,0 +1,25 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

View File

@ -0,0 +1,52 @@
import { useState, useEffect } from 'react';
import PostForm from './PostForm';
import SearchForm from './SearchForm';
import PostList from './PostList';
import Greeting from './Greeting';
import GraphContainer from './GraphContainer';
import { API_URL } from '../config';
import '../App.css';
function App() {
const [posts, setPosts] = useState([]);
const [searchResults, setSearchResults] = useState([]);
const [userVector, setUserVector] = useState(
JSON.parse(localStorage.getItem('userVector')) || null
);
useEffect(() => {
loadPosts();
}, []);
const loadPosts = async () => {
try {
const response = await fetch(`${API_URL}/posts/`);
const data = await response.json();
setPosts(data);
} catch (error) {
console.error('Error loading posts:', error);
}
};
return (
<div className="app">
<Greeting show={!userVector} />
{userVector && <GraphContainer vector={userVector} />}
<PostForm
onPostCreated={(vector) => {
localStorage.setItem('userVector', JSON.stringify(vector));
setUserVector(vector);
loadPosts();
}}
/>
<SearchForm onSearch={setSearchResults} />
<PostList posts={searchResults.length > 0 ? searchResults : posts} />
</div>
);
}
export default App;

View File

@ -0,0 +1,94 @@
import { useState, useEffect, useRef } from 'react';
import vis from 'vis-network/standalone/umd/vis-network.min';
import { API_URL } from '../config';
function GraphContainer({ vector }) {
const [posts, setPosts] = useState([]);
const networkRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
const loadTree = async () => {
try {
const response = await fetch(`${API_URL}/posts/tree`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vector }),
});
const data = await response.json();
// Добавлен лог полученных данных
console.log('Данные, полученные с сервера:', data);
setPosts(data);
} catch (error) {
console.error('Tree load error:', error);
}
};
loadTree();
}, [vector]);
// Остальной код без изменений
useEffect(() => {
if (posts.length > 0 && containerRef.current) {
const { nodes, edges } = createGraphData(posts);
const options = {
nodes: {
borderWidth: 2,
color: { border: '#800', background: '#F0E0D6' },
font: { size: 14 }
},
edges: {
color: '#800',
width: 2,
arrows: 'to',
smooth: { type: 'curvedCW' }
},
physics: {
stabilization: true,
barnesHut: { gravitationalConstant: -2000, springLength: 200 }
}
};
networkRef.current = new vis.Network(
containerRef.current,
{ nodes, edges },
options
);
}
return () => {
if (networkRef.current) {
networkRef.current.destroy();
}
};
}, [posts]);
return (
<div className="graph-container">
<h2>Ризома мыслительная</h2>
<div ref={containerRef} style={{ width: '100%', height: '600px', border: '0px solid #800' }} />
</div>
);
}
function createGraphData(posts) {
const nodes = posts.map(post => ({
id: post.id,
label: post.text || '[Изображение]',
image: post.image ? `${API_URL}/${post.image}` : 'https://via.placeholder.com/100',
shape: 'circularImage',
size: 25
}));
const edges = posts.slice(1).map((post, index) => ({
from: posts[index].id,
to: post.id
}));
return { nodes, edges };
}
export default GraphContainer;

View File

@ -0,0 +1,9 @@
function Greeting({ show }) {
return show ? (
<div id="greeting">
<h2>Отправьте первое сообщение, чтобы начать</h2>
</div>
) : null;
}
export default Greeting;

View File

@ -0,0 +1,61 @@
import { useState } from 'react';
import { API_URL } from '../config';
function PostForm({ onPostCreated }) {
const [text, setText] = useState('');
const [image, setImage] = useState(null);
const [preview, setPreview] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
if (text) formData.append('text', text);
if (image) formData.append('image', image);
try {
const response = await fetch(`${API_URL}/posts/`, {
method: 'POST',
body: formData,
});
if (response.ok) {
const data = await response.json();
onPostCreated(data.vector);
setText('');
setImage(null);
setPreview('');
}
} catch (error) {
console.error('Post creation error:', error);
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
if (file) {
setImage(file);
const reader = new FileReader();
reader.onload = () => setPreview(reader.result);
reader.readAsDataURL(file);
}
};
return (
<div className="post-form">
<h2>Новый пост</h2>
<form onSubmit={handleSubmit}>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
rows="4"
placeholder="Текст поста"
/>
<input type="file" onChange={handleImageChange} accept="image/*" />
{preview && <img src={preview} className="preview-image" alt="Preview" />}
<button type="submit">Отправить</button>
</form>
</div>
);
}
export default PostForm;

View File

@ -0,0 +1,19 @@
import { API_URL } from '../config';
function PostList({ posts }) {
return (
<div id="postsContainer">
{posts.map(post => (
<div key={post.id} className="post">
<div className="post-meta">
#{post.id} - {new Date(post.created_at).toLocaleString()}
</div>
{post.text && <div className="post-text">{post.text}</div>}
{post.image && <img src={`${API_URL}/${post.image}`} alt="Post" />}
</div>
))}
</div>
);
}
export default PostList;

View File

@ -0,0 +1,31 @@
import { useState } from 'react';
import { API_URL } from '../config';
function SearchForm({ onSearch }) {
const [query, setQuery] = useState('');
const handleSearch = async () => {
try {
const response = await fetch(`${API_URL}/search?text=${encodeURIComponent(query)}`);
const results = await response.json();
onSearch(results);
} catch (error) {
console.error('Search error:', error);
}
};
return (
<div className="post-form">
<h2>Поиск</h2>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Текст для поиска"
/>
<button onClick={handleSearch}>Искать</button>
</div>
);
}
export default SearchForm;

1
futa-clone/src/config.js Normal file
View File

@ -0,0 +1 @@
export const API_URL = 'http://localhost:8000';

11
futa-clone/src/index.js Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './components/App';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

270
index.html Normal file
View File

@ -0,0 +1,270 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FutaClone</title>
<!-- Добавляем библиотеку для визуализации графа -->
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
/* Добавляем новые стили */
#network {
width: 100%;
height: 600px;
border: 1px solid #800;
background: white;
}
.hidden { display: none; }
#greeting { text-align: center; padding: 20px; }
#graphContainer { margin-top: 20px; }
/* Стили в духе традиционных имиджборд */
body {
background-color: #F0E0D6;
font-family: Arial, sans-serif;
margin: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.post-form {
background: #FFF;
border: 1px solid #800;
padding: 15px;
margin-bottom: 20px;
}
.post {
background: #FFF;
border: 1px solid #800;
padding: 15px;
margin-bottom: 10px;
}
.post img {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
}
input, textarea, button {
margin: 5px;
padding: 5px;
}
.hidden {
display: none;
}
.preview-image {
max-width: 150px;
margin: 10px 0;
}
</style>
</head>
<body>
<!-- Добавляем новые блоки -->
<div id="greeting" class="hidden">
<h2>Отправьте первое сообщение, чтобы начать</h2>
</div>
<div id="graphContainer" class="hidden">
<h2>Древо мыслей</h2>
<div id="network"></div>
</div>
<!-- Форма создания поста -->
<div class="post-form">
<h2>Новый пост</h2>
<form id="postForm">
<textarea id="postText" rows="4" cols="50" placeholder="Текст поста"></textarea><br>
<input type="file" id="imageInput" accept="image/*"><br>
<div id="imagePreview" class="hidden"></div>
<button type="submit">Отправить</button>
</form>
</div>
<!-- Форма поиска (только текст) -->
<div class="post-form">
<h2>Поиск</h2>
<input type="text" id="searchText" placeholder="Текст для поиска">
<button onclick="searchPosts()">Искать</button>
</div>
<!-- Список постов -->
<div id="postsContainer"></div>
<script>
// Базовый URL бэкенда
const API_URL = 'http://localhost:8000';
// Обработка отправки поста (оставляем без изменений)
document.getElementById('postForm').addEventListener('submit', async (e) => {
e.preventDefault();
const text = document.getElementById('postText').value;
const fileInput = document.getElementById('imageInput');
const formData = new FormData();
if (text) formData.append('text', text);
if (fileInput.files[0]) formData.append('image', fileInput.files[0]);
try {
const response = await fetch(`${API_URL}/posts/`, {
method: 'POST',
body: formData
});
if (response.ok) {
const postData = await response.json();
// Сохраняем вектор в localStorage
localStorage.setItem('userVector', JSON.stringify(postData.vector));
checkUserVector();
loadPosts();
document.getElementById('postForm').reset();
document.getElementById('imagePreview').classList.add('hidden');
} else {
console.error('Error:', error);
}
} catch (error) {
console.error('Error:', error);
}
});
// Превью изображения перед загрузкой
document.getElementById('imageInput').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
preview.innerHTML = `<img src="${e.target.result}" class="preview-image">`;
preview.classList.remove('hidden');
}
reader.readAsDataURL(this.files[0]);
}
});
// Загрузка и отображение постов (оставляем без изменений)
async function loadPosts() {
try {
const response = await fetch(`${API_URL}/posts/`);
const posts = await response.json();
const container = document.getElementById('postsContainer');
container.innerHTML = posts.map(post => `
<div class="post">
<div class="post-meta">#${post.id} - ${new Date(post.created_at).toLocaleString()}</div>
${post.text ? `<div class="post-text">${post.text}</div>` : ''}
${post.image ? `<img src="${API_URL}/${post.image}" />` : ''}
</div>
`).join('');
} catch (error) {
console.error('Error:', error);
}
}
// Обновлённая функция поиска, использующая GET-запрос
async function searchPosts() {
const text = document.getElementById('searchText').value;
// Формируем строку запроса, если введён текст
const query = text ? `?text=${encodeURIComponent(text)}` : '';
try {
const response = await fetch(`${API_URL}/search/${query}`, {
method: 'GET'
});
if (!response.ok) {
throw new Error('Запрос поиска завершился с ошибкой');
}
const results = await response.json();
alert('Найдено постов: ' + results.length);
// Здесь можно добавить отображение результатов поиска в интерфейсе
} catch (error) {
console.error('Error during search:', error);
}
}
// Первоначальная загрузка постов
loadPosts();
// Новая функция проверки вектора
function checkUserVector() {
const hasVector = localStorage.getItem('userVector') !== null;
document.getElementById('greeting').classList.toggle('hidden', hasVector);
document.getElementById('graphContainer').classList.toggle('hidden', !hasVector);
if (hasVector) {
loadUserTree();
} else {
loadPosts();
}
}
// Функция загрузки дерева
async function loadUserTree() {
const userVector = JSON.parse(localStorage.getItem('userVector'));
if (!userVector) return;
try {
const response = await fetch(`${API_URL}/posts/tree`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vector: userVector }),
});
const posts = await response.json();
renderGraph(posts);
} catch (error) {
console.error('Error loading tree:', error);
}
}
// Визуализация графа
function renderGraph(posts) {
const container = document.getElementById('network');
const nodes = [];
const edges = [];
posts.forEach((post, index) => {
nodes.push({
id: post.id,
label: post.text || '[Изображение]',
image: post.image ? `${API_URL}/${post.image}` : 'https://via.placeholder.com/100',
shape: 'circularImage',
size: 25
});
if (index > 0) {
edges.push({
from: posts[index-1].id,
to: post.id,
arrows: 'to',
smooth: { type: 'curvedCW' }
});
}
});
const data = { nodes, edges };
const options = {
nodes: {
borderWidth: 2,
color: {
border: '#800',
background: '#F0E0D6'
},
font: { size: 14 }
},
edges: {
color: '#800',
width: 2
},
physics: {
stabilization: true,
barnesHut: {
gravitationalConstant: -2000,
springLength: 200
}
}
};
new vis.Network(container, data, options);
}
// Инициализация при загрузке страницы
checkUserVector();
</script>
</body>
</html>

297
main.py Normal file
View File

@ -0,0 +1,297 @@
# main.py
from fastapi import FastAPI, UploadFile, File, HTTPException, Form
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import List, Optional
import uuid
import os
from datetime import datetime
import requests
from qdrant_client import QdrantClient
from qdrant_client.http import models
from fastapi.middleware.cors import CORSMiddleware
# Database setup
from sqlalchemy import create_engine, Column, String, DateTime, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Image processing
from PIL import Image
import io
import torch
from transformers import CLIPProcessor, CLIPModel
import logging
from qdrant_client.http.models import VectorParams, Distance
# Настройка базового логирования (например, вывод в консоль)
logging.basicConfig(
level=logging.INFO, # Можно изменить уровень, например, на DEBUG
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Задайте имя коллекции
COLLECTION_NAME = "posts"
# Определите размер вектора. Этот размер должен соответствовать длине объединённого эмбеддинга текста и изображения.
VECTOR_SIZE = 1280 # Пример: поменяйте на актуальное значение для вашего случая
# Configuration
DATABASE_URL = "sqlite:///./imageboard.db"
QDRANT_URL = "http://localhost:6333"
OLLAMA_URL = "http://localhost:11434"
EMBEDDING_MODEL = "nomic-embed-text" # Локальная модель через Ollama
IMAGE_MODEL = "openai/clip-vit-base-patch32" # Локальная CLIP модель
IMAGE_SIZE = (224, 224)
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Initialize components
Base = declarative_base()
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
#Base.metadata.drop_all(bind=engine) # Удаляет все таблицы
#Base.metadata.create_all(bind=engine) # Создаёт таблицы заново
# Инициализация CLIP для изображений
clip_model = CLIPModel.from_pretrained(IMAGE_MODEL)
clip_processor = CLIPProcessor.from_pretrained(IMAGE_MODEL)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
clip_model = clip_model.to(device)
# Qdrant клиент
qdrant_client = QdrantClient(QDRANT_URL)
def ensure_collection_exists():
try:
# Попытка получить коллекцию. Если коллекция не существует, Qdrant выбросит исключение.
qdrant_client.get_collection(collection_name=COLLECTION_NAME)
logger.info("Коллекция '%s' существует.", COLLECTION_NAME)
except Exception as e:
logger.info("Коллекция '%s' не найдена. Создаём коллекцию...", COLLECTION_NAME)
qdrant_client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(
size=VECTOR_SIZE,
distance=Distance.COSINE # Или другой подходящий тип расстояния
)
)
logger.info("Коллекция '%s' создана.", COLLECTION_NAME)
# Вызываем функцию при инициализации приложения, например, в начале main.py
ensure_collection_exists()
app = FastAPI()
app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name=UPLOAD_DIR)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Разрешить все источники
allow_credentials=True,
allow_methods=["*"], # Разрешить все методы
allow_headers=["*"], # Разрешить все заголовки
)
# Database models
class Post(Base):
__tablename__ = "posts"
id = Column(String, primary_key=True, index=True)
text = Column(Text, nullable=True)
image = Column(String, nullable=True)
created_at = Column(DateTime)
Base.metadata.create_all(bind=engine)
# Pydantic model для ответа
class PostResponse(BaseModel):
id: str
text: Optional[str] = None
image: Optional[str] = None
created_at: datetime
vector: Optional[List[float]] = None
class Config:
orm_mode = True
# Pydantic модель для запроса вектора
class VectorQuery(BaseModel):
vector: List[float]
# Utility functions
def generate_text_embedding(text: str) -> List[float]:
response = requests.post(
f"{OLLAMA_URL}/api/embeddings",
json={"model": EMBEDDING_MODEL, "prompt": text}
)
if response.status_code != 200:
raise HTTPException(status_code=500, detail="Embedding generation failed")
return response.json()["embedding"]
def generate_image_embedding(image_bytes: bytes) -> List[float]:
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
inputs = clip_processor(
images=image,
return_tensors="pt",
padding=True
).to(device)
with torch.no_grad():
features = clip_model.get_image_features(**inputs)
return features.cpu().numpy().tolist()[0]
def process_image(image_bytes: bytes) -> bytes:
img = Image.open(io.BytesIO(image_bytes))
img = img.convert("RGB")
img = img.resize(IMAGE_SIZE)
buffer = io.BytesIO()
img.save(buffer, format="JPEG")
return buffer.getvalue()
# API endpoints
@app.post("/posts/", response_model=PostResponse)
async def create_post(
text: Optional[str] = Form(None),
image: Optional[UploadFile] = File(None)
):
db = SessionLocal()
try:
post_id = str(uuid.uuid4())
image_path = None
embeddings = []
if text:
logger.info("Генерация эмбеддинга для текста")
text_embedding = generate_text_embedding(text)
embeddings.extend(text_embedding)
if image:
logger.info("Обработка изображения")
image_bytes = await image.read()
processed_image = process_image(image_bytes)
image_path = f"{UPLOAD_DIR}/{post_id}.jpg"
with open(image_path, "wb") as f:
f.write(processed_image)
image_embedding = generate_image_embedding(processed_image)
embeddings.extend(image_embedding)
logger.info("Сохранение данных в Qdrant")
qdrant_client.upsert(
collection_name="posts",
points=[models.PointStruct(
id=post_id,
vector=embeddings,
payload={"post_id": post_id}
)]
)
logger.info("Сохранение поста в базу данных")
db_post = Post(
id=post_id,
text=text,
image=image_path,
created_at=datetime.now()
)
db.add(db_post)
db.commit()
db.refresh(db_post)
response = PostResponse(
id=db_post.id,
text=db_post.text,
image=db_post.image,
created_at=db_post.created_at,
vector=embeddings
)
logger.info("Пост успешно создан: %s", response)
return response
except Exception as e:
db.rollback()
logger.exception("Ошибка при создании поста")
raise HTTPException(status_code=500, detail=str(e))
finally:
db.close()
@app.get("/search/")
async def search_posts(
text: Optional[str] = None,
image: Optional[UploadFile] = File(None)
):
try:
query_embedding = []
if text:
logger.info("Генерация эмбеддинга для текста (поиск)")
text_embedding = generate_text_embedding(text)
query_embedding.extend(text_embedding)
if image:
logger.info("Генерация эмбеддинга для изображения (поиск)")
image_bytes = await image.read()
processed_image = process_image(image_bytes)
image_embedding = generate_image_embedding(processed_image)
query_embedding.extend(image_embedding)
logger.info("Выполнение поиска в Qdrant")
search_results = qdrant_client.search(
collection_name="posts",
query_vector=query_embedding,
limit=10
)
logger.info("Поиск завершён. Найдено результатов: %d", len(search_results))
return [result.payload for result in search_results]
except Exception as e:
logger.exception("Ошибка при поиске постов")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/posts/", response_model=List[PostResponse])
async def get_all_posts():
db = SessionLocal()
try:
posts = db.query(Post).all()
return posts
finally:
db.close()
# Новый endpoint: получение "древа" постов по вектору пользователя
@app.post("/posts/tree", response_model=List[PostResponse])
async def get_posts_tree(query: VectorQuery):
# Выполняем поиск в Qdrant с большим лимитом, чтобы получить все посты, отсортированные по сходству
search_results = qdrant_client.search(
collection_name="posts",
query_vector=query.vector,
limit=10000 # Задайте лимит в зависимости от ожидаемого числа постов
)
print("search_results")
print(search_results)
# Извлекаем список ID постов в том порядке, в котором Qdrant вернул результаты (от ближайших к дальним)
post_ids = [result.payload.get("post_id") for result in search_results]
print("post_ids")
print(post_ids)
db = SessionLocal()
try:
# Получаем все посты из БД по списку ID
posts = db.query(Post).filter(Post.id.in_(post_ids)).all()
print("posts")
print(posts)
# Создаём словарь для сохранения соответствия post_id -> post
posts_dict = {post.id: post for post in posts}
print("posts_dict")
print(posts_dict)
# Восстанавливаем порядок, используя список post_ids
ordered_posts = [posts_dict[pid] for pid in post_ids if pid in posts_dict]
print("ordered_posts")
print(ordered_posts)
return ordered_posts
finally:
db.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
# requirements.txt
fastapi
uvicorn
python-multipart
qdrant-client
sqlalchemy
requests
pillow
transformers
torch