windopper commited on
Commit
9441c7f
ยท
2 Parent(s): d25d1dfa78c144

Merge pull request #1 from caainp/dev

Browse files
.vercelignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Vercel์ด uv ๋Œ€์‹  pip๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก uv ๊ด€๋ จ ํŒŒ์ผ ์ œ์™ธ
2
+ # ์ด ํŒŒ์ผ๋“ค์ด ์—…๋กœ๋“œ๋˜์ง€ ์•Š์œผ๋ฉด Vercel์€ requirements.txt์™€ pip๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค
3
+ ; pyproject.toml
4
+ ; uv.lock
5
+ ; .venv
6
+ ; __pycache__
7
+ ; *.pyc
8
+ ; tests/
9
+ ; images/
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "python-envs.pythonProjects": []
3
+ }
README.md CHANGED
@@ -27,15 +27,17 @@ uvicorn server:app --reload
27
  API ๋ฌธ์„œ๋Š” `http://localhost:8000/docs`์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
28
 
29
  ## API ์—”๋“œํฌ์ธํŠธ
30
- ### POST `/api/navigation/step`
31
-
32
- ๋„ค๋น„๊ฒŒ์ด์…˜ ๋‹จ๊ณ„๋ณ„ ์•ˆ๋‚ด๋ฅผ ์š”์ฒญ
33
 
34
- **์š”์ฒญ:**
35
- - `request_text` (Form): ์ž์—ฐ์–ด ์š”์ฒญ ํ…์ŠคํŠธ
36
- - `image` (File): ํ˜„์žฅ ์ด๋ฏธ์ง€ ํŒŒ์ผ
 
37
 
38
- **์‘๋‹ต ์˜ˆ์‹œ:**
 
 
 
 
39
  ```json
40
  {
41
  "schema_version": 1,
@@ -55,6 +57,28 @@ API ๋ฌธ์„œ๋Š” `http://localhost:8000/docs`์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
55
  }
56
  ```
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  ## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
59
 
60
  ```
@@ -64,3 +88,4 @@ caanip-backend/
64
  โ”œโ”€โ”€ pyproject.toml # ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ ์˜์กด์„ฑ
65
  โ””โ”€โ”€ README.md # ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ
66
  ```
 
 
27
  API ๋ฌธ์„œ๋Š” `http://localhost:8000/docs`์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
28
 
29
  ## API ์—”๋“œํฌ์ธํŠธ
 
 
 
30
 
31
+ ### POST `/api/navigation/start`
32
+ - ์ž์—ฐ์–ด ์š”์ฒญ๊ณผ ์‹œ์ž‘ ๋ฐฉ, ํ˜„์žฅ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„ ๋„ค๋น„๊ฒŒ์ด์…˜ ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๊ณ  ์ฒซ ์•ˆ๋‚ด๋ฅผ ๋ฐ˜ํ™˜
33
+ - Form ๋ฐ์ดํ„ฐ: `request_text`(์ž์—ฐ์–ด ์š”์ฒญ), `start_room`(์‹œ์ž‘ ๋ฐฉ ๋ฒˆํ˜ธ), File: `image`(ํ˜„์žฅ ์ด๋ฏธ์ง€)
34
+ - ์‘๋‹ต: ์ฒซ ๋„ค๋น„๊ฒŒ์ด์…˜ ์•ˆ๋‚ด(JSON). ์„ธ์…˜ ์ƒํƒœ๊ฐ€ `CAAINP-NAVIGATION-COOKIE`์— ์ €์žฅ๋จ.
35
 
36
+ ### POST `/api/navigation/step`
37
+ - ์ฟ ํ‚ค์— ์ €์žฅ๋œ ์„ธ์…˜ ์ƒํƒœ(plan, prev_node)๋ฅผ ์‚ฌ์šฉํ•ด ๋‹ค์Œ ๋‹จ๊ณ„ ์•ˆ๋‚ด๋ฅผ ๋ฐ˜ํ™˜
38
+ - File: `image`(ํ˜„์žฅ ์ด๋ฏธ์ง€)
39
+ - ์„ธ์…˜์ด ์—†์œผ๋ฉด `{ "status": "error", "message": "/api/navigation/start๋ฅผ ๋จผ์ € ํ˜ธ์ถœํ•˜์„ธ์š”." }` ๋ฐ˜ํ™˜
40
+ - ์‘๋‹ต ์˜ˆ์‹œ:
41
  ```json
42
  {
43
  "schema_version": 1,
 
57
  }
58
  ```
59
 
60
+ ### POST `/api/navigation/reset`
61
+ - ์ €์žฅ๋œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฟ ํ‚ค ์‚ญ์ œ ํ›„ ์„ธ์…˜ ์ดˆ๊ธฐํ™”
62
+ - ์‘๋‹ต: `{ "status": "success", "message": "๋„ค๋น„๊ฒŒ์ด์…˜ ์„ธ์…˜์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }`
63
+
64
+ ### POST `/api/navigation/mock`
65
+ - ์‹ค์ œ ์—”์ง„ ๋Œ€์‹  ๋ชจ์˜ ์‘๋‹ต ๋ฐ˜ํ™˜(๋””๋ฒ„๊น…/ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™์šฉ)
66
+ - Form: `request_text`, File: `image`
67
+ - ์‘๋‹ต: ๋ชจ์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ JSON
68
+
69
+ ### GET `/api/cookie/get`
70
+ - ํ˜„์žฌ ์š”์ฒญ์˜ `CAAINP-NAVIGATION-COOKIE` ๊ฐ’์„ ์กฐํšŒ
71
+ - ์‘๋‹ต: `{ "cookie": {plan, prev_node} | null }`
72
+
73
+ ### GET `/api/cookie/delete`
74
+ - ๋„ค๋น„๊ฒŒ์ด์…˜ ์ฟ ํ‚ค ์‚ญ์ œ
75
+ - ์‘๋‹ต: `{ "status": "success", "message": "์ฟ ํ‚ค๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }`
76
+
77
+ ## ํ…Œ์ŠคํŠธ
78
+ ```bash
79
+ uv run tests/navigation_test.py
80
+ ```
81
+
82
  ## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
83
 
84
  ```
 
88
  โ”œโ”€โ”€ pyproject.toml # ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ ์˜์กด์„ฑ
89
  โ””โ”€โ”€ README.md # ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ
90
  ```
91
+
api/index.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Vercel Serverless Function Entry Point
2
+ from server import app
3
+
core/cookie.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, Response
2
+ from caainp_csm.plan_csm import PlanState, Constraints, Step
3
+ import json
4
+
5
+ COOKIE_KEY = "CAAINP-NAVIGATION-COOKIE"
6
+
7
+
8
+ def plan_to_dict(plan: PlanState) -> dict:
9
+ """PlanState๋ฅผ JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ dict๋กœ ๋ณ€ํ™˜"""
10
+ return {
11
+ "constraints": {
12
+ "use_elevator_only": plan.constraints.use_elevator_only,
13
+ "avoid_stairs": plan.constraints.avoid_stairs,
14
+ "forbidden_nodes": plan.constraints.forbidden_nodes,
15
+ "via_rooms": plan.constraints.via_rooms,
16
+ },
17
+ "steps": [
18
+ {
19
+ "step_id": s.step_id,
20
+ "goal_type": s.goal_type,
21
+ "goal_room": s.goal_room,
22
+ "allowed_moves": s.allowed_moves,
23
+ "description_ko": s.description_ko,
24
+ "target_nodes": s.target_nodes,
25
+ "route_nodes": s.route_nodes,
26
+ }
27
+ for s in plan.steps
28
+ ],
29
+ "current_step": plan.current_step,
30
+ "steps_status": plan.steps_status,
31
+ "step_targets": {str(k): v for k, v in plan.step_targets.items()},
32
+ "in_target_count": plan.in_target_count,
33
+ }
34
+
35
+
36
+ def dict_to_plan(data: dict) -> PlanState:
37
+ """dict๋ฅผ PlanState ๊ฐ์ฒด๋กœ ๋ณต์›"""
38
+ constraints = Constraints(
39
+ use_elevator_only=data["constraints"]["use_elevator_only"],
40
+ avoid_stairs=data["constraints"]["avoid_stairs"],
41
+ forbidden_nodes=data["constraints"]["forbidden_nodes"],
42
+ via_rooms=data["constraints"]["via_rooms"],
43
+ )
44
+
45
+ steps = [
46
+ Step(
47
+ step_id=s["step_id"],
48
+ goal_type=s["goal_type"],
49
+ goal_room=s["goal_room"],
50
+ allowed_moves=s["allowed_moves"],
51
+ description_ko=s["description_ko"],
52
+ target_nodes=s["target_nodes"],
53
+ route_nodes=s["route_nodes"],
54
+ )
55
+ for s in data["steps"]
56
+ ]
57
+
58
+ # steps_status์˜ ํ‚ค๋ฅผ int๋กœ ๋ณต์›
59
+ steps_status = {int(k): v for k, v in data["steps_status"].items()}
60
+ # step_targets์˜ ํ‚ค๋ฅผ int๋กœ ๋ณต์›
61
+ step_targets = {int(k): v for k, v in data["step_targets"].items()}
62
+
63
+ return PlanState(
64
+ constraints=constraints,
65
+ steps=steps,
66
+ current_step=data["current_step"],
67
+ steps_status=steps_status,
68
+ step_targets=step_targets,
69
+ in_target_count=data["in_target_count"],
70
+ )
71
+
72
+
73
+ def get_nav_state_from_cookie(request: Request) -> tuple[PlanState, int] | None:
74
+ """์ฟ ํ‚ค์—์„œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒํƒœ๋ฅผ ์ฝ์–ด์„œ ๋ฐ˜ํ™˜"""
75
+ cookies = dict(request.cookies)
76
+ nav_state_cookie = cookies.get(COOKIE_KEY)
77
+
78
+ if not nav_state_cookie:
79
+ return None
80
+
81
+ try:
82
+ nav_state = json.loads(nav_state_cookie)
83
+ plan_dict = nav_state.get("plan")
84
+ prev_node = nav_state.get("prev_node")
85
+ plan = dict_to_plan(plan_dict)
86
+ return plan, prev_node
87
+ except (json.JSONDecodeError, KeyError, TypeError):
88
+ return None
89
+
90
+
91
+ def save_nav_state_to_cookie(response: Response, plan: PlanState, prev_node: int) -> None:
92
+ """๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒํƒœ(plan, prev_node)๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅ"""
93
+ nav_state = {
94
+ "plan": plan_to_dict(plan),
95
+ "prev_node": prev_node,
96
+ }
97
+ response.set_cookie(
98
+ key=COOKIE_KEY,
99
+ value=json.dumps(nav_state),
100
+ max_age=3600,
101
+ httponly=True,
102
+ samesite="lax"
103
+ )
104
+
105
+
106
+ def clear_nav_cookie(response: Response) -> None:
107
+ """๋„ค๋น„๊ฒŒ์ด์…˜ ์ฟ ํ‚ค ์‚ญ์ œ"""
108
+ response.delete_cookie(COOKIE_KEY)
109
+
core/navigation.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from caainp_csm.plan_csm import PlanState
2
+ from caainp_cvm.nav_engine import init_plan, compute_nav_output, _get_csv_path
3
+
4
+ CSV_PATH = _get_csv_path()
5
+
6
+ # ๋„ค๋น„๊ฒŒ์ด์…˜ ์‹œ์ž‘
7
+ def start_navigation(user_text: str, start_room: int, image_path: str):
8
+ plan, _ = init_plan(
9
+ user_text=user_text,
10
+ start_room=start_room,
11
+ )
12
+
13
+ out = compute_nav_output(
14
+ image_path=str(image_path),
15
+ plan=plan,
16
+ prev_node=None,
17
+ csv_path=str(CSV_PATH),
18
+ )
19
+
20
+ nav_output = out["nav_output"]
21
+ plan = out["plan"]
22
+ prev_node = nav_output["current_node"]
23
+
24
+ return plan, nav_output, prev_node
25
+
26
+ # ์ฟ ํ‚ค ๊ฐ’์— ์ €์žฅ๋œ plan, prev_node์ด ์žˆ๋‹ค๋ฉด compute_nav_output ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜
27
+ def navigation_step(plan, prev_node, image_path: str):
28
+ out = compute_nav_output(
29
+ image_path=str(image_path),
30
+ plan=plan,
31
+ prev_node=prev_node,
32
+ csv_path=str(CSV_PATH),
33
+ )
34
+
35
+ nav_output = out["nav_output"]
36
+ plan = out["plan"]
37
+ prev_node = nav_output["current_node"]
38
+
39
+ return plan, nav_output, prev_node
pyproject.toml CHANGED
@@ -8,4 +8,11 @@ dependencies = [
8
  "fastapi>=0.123.10",
9
  "uvicorn>=0.38.0",
10
  "python-multipart>=0.0.6",
 
 
11
  ]
 
 
 
 
 
 
8
  "fastapi>=0.123.10",
9
  "uvicorn>=0.38.0",
10
  "python-multipart>=0.0.6",
11
+ "caainp-csm",
12
+ "caainp-cvm",
13
  ]
14
+
15
+ [tool.uv.sources]
16
+ caainp-csm = { git = "https://github.com/caainp/caainp-csm.git" }
17
+ caainp-cvm = { git = "https://github.com/caainp/caainp-cvm.git" }
18
+
requirements.txt ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile pyproject.toml -o requirements.txt
3
+ annotated-doc==0.0.4
4
+ # via fastapi
5
+ annotated-types==0.7.0
6
+ # via pydantic
7
+ anyio==4.12.0
8
+ # via starlette
9
+ caainp-csm @ git+https://github.com/caainp/caainp-csm.git@e3d6db43435f1744470a87c5268d2503d187f067
10
+ # via
11
+ # caanip-backend (pyproject.toml)
12
+ # caainp-cvm
13
+ caainp-cvm @ git+https://github.com/caainp/caainp-cvm.git@99fcde434ab55b951527d2cdd51dc196cb5fe6db
14
+ # via caanip-backend (pyproject.toml)
15
+ certifi==2025.11.12
16
+ # via requests
17
+ charset-normalizer==3.4.4
18
+ # via requests
19
+ click==8.3.1
20
+ # via uvicorn
21
+ colorama==0.4.6
22
+ # via
23
+ # click
24
+ # loguru
25
+ # tqdm
26
+ contourpy==1.3.3
27
+ # via matplotlib
28
+ cycler==0.12.1
29
+ # via matplotlib
30
+ easyocr==1.7.2
31
+ # via caainp-cvm
32
+ fastapi==0.124.0
33
+ # via caanip-backend (pyproject.toml)
34
+ filelock==3.20.0
35
+ # via
36
+ # huggingface-hub
37
+ # torch
38
+ # transformers
39
+ fonttools==4.61.0
40
+ # via matplotlib
41
+ fsspec==2025.12.0
42
+ # via
43
+ # huggingface-hub
44
+ # torch
45
+ ftfy==6.3.1
46
+ # via open-clip-torch
47
+ h11==0.16.0
48
+ # via uvicorn
49
+ hf-xet==1.2.0
50
+ # via caainp-cvm
51
+ huggingface-hub==0.36.0
52
+ # via
53
+ # open-clip-torch
54
+ # timm
55
+ # tokenizers
56
+ # transformers
57
+ idna==3.11
58
+ # via
59
+ # anyio
60
+ # requests
61
+ imageio==2.37.2
62
+ # via scikit-image
63
+ jinja2==3.1.6
64
+ # via torch
65
+ joblib==1.5.2
66
+ # via scikit-learn
67
+ kiwisolver==1.4.9
68
+ # via matplotlib
69
+ lazy-loader==0.4
70
+ # via scikit-image
71
+ loguru==0.7.3
72
+ # via caainp-cvm
73
+ markupsafe==3.0.3
74
+ # via jinja2
75
+ matplotlib==3.10.7
76
+ # via
77
+ # caainp-cvm
78
+ # seaborn
79
+ mpmath==1.3.0
80
+ # via sympy
81
+ networkx==3.6
82
+ # via
83
+ # caainp-cvm
84
+ # scikit-image
85
+ # torch
86
+ ninja==1.13.0
87
+ # via easyocr
88
+ numpy==2.3.5
89
+ # via
90
+ # caainp-cvm
91
+ # contourpy
92
+ # easyocr
93
+ # imageio
94
+ # matplotlib
95
+ # opencv-contrib-python
96
+ # opencv-python-headless
97
+ # pandas
98
+ # scikit-image
99
+ # scikit-learn
100
+ # scipy
101
+ # seaborn
102
+ # shapely
103
+ # tifffile
104
+ # torchvision
105
+ # transformers
106
+ open-clip-torch==3.2.0
107
+ # via caainp-cvm
108
+ opencv-contrib-python==4.11.0.86
109
+ # via caainp-cvm
110
+ opencv-python-headless==4.11.0.86
111
+ # via easyocr
112
+ packaging==25.0
113
+ # via
114
+ # huggingface-hub
115
+ # lazy-loader
116
+ # matplotlib
117
+ # scikit-image
118
+ # transformers
119
+ pandas==2.3.3
120
+ # via
121
+ # caainp-csm
122
+ # caainp-cvm
123
+ # seaborn
124
+ pillow==12.0.0
125
+ # via
126
+ # caainp-cvm
127
+ # easyocr
128
+ # imageio
129
+ # matplotlib
130
+ # scikit-image
131
+ # torchvision
132
+ pyclipper==1.4.0
133
+ # via easyocr
134
+ pydantic==2.12.5
135
+ # via fastapi
136
+ pydantic-core==2.41.5
137
+ # via pydantic
138
+ pyparsing==3.2.5
139
+ # via matplotlib
140
+ python-bidi==0.6.7
141
+ # via easyocr
142
+ python-dateutil==2.9.0.post0
143
+ # via
144
+ # matplotlib
145
+ # pandas
146
+ python-multipart==0.0.20
147
+ # via caanip-backend (pyproject.toml)
148
+ pytz==2025.2
149
+ # via pandas
150
+ pyyaml==6.0.3
151
+ # via
152
+ # easyocr
153
+ # huggingface-hub
154
+ # timm
155
+ # transformers
156
+ regex==2025.11.3
157
+ # via
158
+ # open-clip-torch
159
+ # transformers
160
+ requests==2.32.5
161
+ # via
162
+ # huggingface-hub
163
+ # transformers
164
+ safetensors==0.7.0
165
+ # via
166
+ # open-clip-torch
167
+ # timm
168
+ # transformers
169
+ scikit-image==0.25.2
170
+ # via
171
+ # caainp-cvm
172
+ # easyocr
173
+ scikit-learn==1.7.2
174
+ # via caainp-cvm
175
+ scipy==1.16.3
176
+ # via
177
+ # easyocr
178
+ # scikit-image
179
+ # scikit-learn
180
+ seaborn==0.13.2
181
+ # via caainp-cvm
182
+ setuptools==80.9.0
183
+ # via torch
184
+ shapely==2.1.2
185
+ # via
186
+ # caainp-cvm
187
+ # easyocr
188
+ six==1.17.0
189
+ # via python-dateutil
190
+ starlette==0.50.0
191
+ # via fastapi
192
+ sympy==1.14.0
193
+ # via torch
194
+ threadpoolctl==3.6.0
195
+ # via scikit-learn
196
+ tifffile==2025.10.16
197
+ # via scikit-image
198
+ timm==1.0.22
199
+ # via open-clip-torch
200
+ tokenizers==0.22.1
201
+ # via
202
+ # caainp-cvm
203
+ # transformers
204
+ torch==2.9.1+cpu
205
+ # via
206
+ # caainp-cvm
207
+ # easyocr
208
+ # open-clip-torch
209
+ # timm
210
+ # torchvision
211
+ torchvision==0.24.1
212
+ # via
213
+ # caainp-cvm
214
+ # easyocr
215
+ # open-clip-torch
216
+ # timm
217
+ tqdm==4.67.1
218
+ # via
219
+ # huggingface-hub
220
+ # open-clip-torch
221
+ # transformers
222
+ transformers==4.57.3
223
+ # via caainp-cvm
224
+ typing-extensions==4.15.0
225
+ # via
226
+ # anyio
227
+ # fastapi
228
+ # huggingface-hub
229
+ # pydantic
230
+ # pydantic-core
231
+ # starlette
232
+ # torch
233
+ # typing-inspection
234
+ typing-inspection==0.4.2
235
+ # via pydantic
236
+ tzdata==2025.2
237
+ # via pandas
238
+ urllib3==2.6.0
239
+ # via requests
240
+ uvicorn==0.38.0
241
+ # via caanip-backend (pyproject.toml)
242
+ wcwidth==0.2.14
243
+ # via ftfy
244
+ win32-setctime==1.2.0
245
+ # via loguru
routes/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+
routes/cookie.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, Response
2
+ from core.cookie import (
3
+ get_nav_state_from_cookie,
4
+ save_nav_state_to_cookie,
5
+ clear_nav_cookie,
6
+ COOKIE_KEY,
7
+ )
8
+
9
+ router = APIRouter(prefix="/api/cookie", tags=["cookie"])
10
+
11
+ @router.get("/get")
12
+ async def cookie_get(request: Request):
13
+ """์ฟ ํ‚ค ์ฝ๊ธฐ ํ…Œ์ŠคํŠธ ์—”๋“œํฌ์ธํŠธ"""
14
+ cookies = dict(request.cookies)
15
+ return {
16
+ "cookie": cookies.get(COOKIE_KEY)
17
+ }
18
+
19
+ @router.get("/delete")
20
+ async def cookie_delete(request: Request, response: Response):
21
+ """์ฟ ํ‚ค ์‚ญ์ œ ํ…Œ์ŠคํŠธ ์—”๋“œํฌ์ธํŠธ"""
22
+ clear_nav_cookie(response)
23
+ return {
24
+ "status": "success",
25
+ "message": "์ฟ ํ‚ค๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
26
+ }
routes/navigation.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, Form, Request, Response
2
+ from core.navigation import start_navigation, navigation_step as nav_step
3
+ from core.cookie import (
4
+ get_nav_state_from_cookie,
5
+ save_nav_state_to_cookie,
6
+ clear_nav_cookie,
7
+ )
8
+ from mock_responses import generate_mock_navigation_response
9
+ import tempfile
10
+ import os
11
+
12
+ router = APIRouter(prefix="/api/navigation", tags=["navigation"])
13
+
14
+
15
+ @router.post("/step")
16
+ async def navigation_step_route(
17
+ request: Request,
18
+ response: Response,
19
+ image: UploadFile = File(..., description="์ด๋ฏธ์ง€"),
20
+ ):
21
+ # ์ฟ ํ‚ค์—์„œ plan๊ณผ prev_node ์ฝ๊ธฐ
22
+ nav_state = get_nav_state_from_cookie(request)
23
+
24
+ if not nav_state:
25
+ return {
26
+ "status": "error",
27
+ "message": "๋„ค๋น„๊ฒŒ์ด์…˜ ์„ธ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. /api/navigation/start๋ฅผ ๋จผ์ € ํ˜ธ์ถœํ•˜์„ธ์š”."
28
+ }
29
+
30
+ plan, prev_node = nav_state
31
+
32
+ # ์ด๋ฏธ์ง€๋ฅผ ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
33
+ image_content = await image.read()
34
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(image.filename)[1]) as tmp_file:
35
+ tmp_file.write(image_content)
36
+ tmp_path = tmp_file.name
37
+
38
+ try:
39
+ # navigation_step ํ˜ธ์ถœ
40
+ plan, nav_output, prev_node = nav_step(
41
+ plan=plan,
42
+ prev_node=prev_node,
43
+ image_path=tmp_path,
44
+ )
45
+
46
+ # ์ฟ ํ‚ค์— ์ƒํƒœ ์ €์žฅ
47
+ save_nav_state_to_cookie(response, plan, prev_node)
48
+
49
+ return nav_output
50
+ finally:
51
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
52
+ if os.path.exists(tmp_path):
53
+ os.unlink(tmp_path)
54
+
55
+
56
+ @router.post("/start")
57
+ async def navigation_start_route(
58
+ response: Response,
59
+ request_text: str = Form(..., description="์ž์—ฐ์–ด ์š”์ฒญ"),
60
+ start_room: int = Form(..., description="์‹œ์ž‘ ๋ฐฉ"),
61
+ image: UploadFile = File(..., description="์ด๋ฏธ์ง€"),
62
+ ):
63
+ # ๊ธฐ์กด ์ฟ ํ‚ค ์ดˆ๊ธฐํ™” (์ƒˆ ์„ธ์…˜ ์‹œ์ž‘)
64
+ clear_nav_cookie(response)
65
+
66
+ # ์ด๋ฏธ์ง€๋ฅผ ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
67
+ image_content = await image.read()
68
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(image.filename)[1]) as tmp_file:
69
+ tmp_file.write(image_content)
70
+ tmp_path = tmp_file.name
71
+
72
+ try:
73
+ # start_navigation ํ˜ธ์ถœ
74
+ plan, nav_output, prev_node = start_navigation(
75
+ user_text=request_text,
76
+ start_room=start_room,
77
+ image_path=tmp_path,
78
+ )
79
+
80
+ # ์ฟ ํ‚ค์— ์ƒํƒœ ์ €์žฅ
81
+ save_nav_state_to_cookie(response, plan, prev_node)
82
+
83
+ return nav_output
84
+ finally:
85
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
86
+ if os.path.exists(tmp_path):
87
+ os.unlink(tmp_path)
88
+
89
+
90
+ @router.post("/reset")
91
+ async def navigation_reset_route(
92
+ response: Response,
93
+ ):
94
+ clear_nav_cookie(response)
95
+ return {
96
+ "status": "success",
97
+ "message": "๋„ค๋น„๊ฒŒ์ด์…˜ ์„ธ์…˜์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
98
+ }
99
+
100
+ @router.post("/mock")
101
+ async def navigation_mock(
102
+ request_text: str = Form(..., description="์ž์—ฐ์–ด ์š”์ฒญ"),
103
+ image: UploadFile = File(..., description="์ด๋ฏธ์ง€"),
104
+ ):
105
+ await image.read()
106
+ response = generate_mock_navigation_response()
107
+ return response
108
+
server.py CHANGED
@@ -1,22 +1,18 @@
1
- from fastapi import FastAPI, UploadFile, File, Form
2
  from datetime import datetime
3
- from mock_responses import generate_mock_navigation_response
 
4
 
5
  app = FastAPI()
6
 
 
 
 
 
7
  @app.get("/api/health")
8
  def health_check():
9
  return {
10
  "status": "healthy",
11
  "timestamp": datetime.now().isoformat(),
12
  "service": "caanip-backend"
13
- }
14
-
15
- @app.post("/api/navigation/step")
16
- async def navigation_step(
17
- request_text: str = Form(..., description="์ž์—ฐ์–ด ์š”์ฒญ"),
18
- image: UploadFile = File(..., description="์ด๋ฏธ์ง€"),
19
- ):
20
- await image.read()
21
- response = generate_mock_navigation_response()
22
- return response
 
1
+ from fastapi import FastAPI, Request, Response
2
  from datetime import datetime
3
+ from routes.navigation import router as navigation_router
4
+ from routes.cookie import router as cookie_router
5
 
6
  app = FastAPI()
7
 
8
+ # ๋ผ์šฐํ„ฐ ๋“ฑ๋ก
9
+ app.include_router(navigation_router)
10
+ app.include_router(cookie_router)
11
+
12
  @app.get("/api/health")
13
  def health_check():
14
  return {
15
  "status": "healthy",
16
  "timestamp": datetime.now().isoformat(),
17
  "service": "caanip-backend"
18
+ }
 
 
 
 
 
 
 
 
 
tests/navigation_test.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ # ํ˜„์žฌ ํŒŒ์ผ ์œ„์น˜: .../nav/caanip-backend/nav_bridge.py
5
+ THIS_DIR = Path(__file__).resolve().parent.parent
6
+ BACKEND_ROOT = THIS_DIR # .../caanip-backend
7
+
8
+ from caainp_csm.graph_4f import Graph4F
9
+ from caainp_csm.plan_csm import (Constraints, Step, PlanState, create_simple_plan, update_state_with_node)
10
+ from caainp_cvm.nav_engine import init_plan, compute_nav_output, _get_csv_path
11
+
12
+ CSV_PATH = _get_csv_path()
13
+
14
+ def quick_test():
15
+ """
16
+ backend ๋ ˆํฌ์—์„œ CSM + CVM + nav_engine์ด ์ œ๋Œ€๋กœ ์—ฐ๊ฒฐ๋˜๋Š”์ง€ ํ™•์ธ์šฉ.
17
+ ์‹ค์ œ FastAPI ์ฝ”๋“œ์—์„œ๋Š” init_plan / compute_nav_output๋งŒ ๊ฐ€์ ธ๋‹ค ์“ฐ๋ฉด ๋จ.
18
+ """
19
+
20
+ plan, _ = init_plan(
21
+ user_text="401์—์„œ 410ํ˜ธ๊นŒ์ง€ ์ด๋™",
22
+ start_room=401,
23
+ )
24
+
25
+ node_images_dir = BACKEND_ROOT / "images"
26
+ img_seq = ["401(1).jpg", "403(3).jpg", "410(1).jpg"]
27
+
28
+ prev_node = None
29
+
30
+ for img_name in img_seq:
31
+ image_path = node_images_dir / img_name
32
+
33
+ out = compute_nav_output(
34
+ image_path=str(image_path),
35
+ plan=plan, # PlanState ๊ฐ์ฒด (์šฐ๋ฆฌ๊ฐ€ nav_engine์—์„œ ์“ฐ๋Š” ๋ฒ„์ „ ๊ธฐ์ค€)
36
+ prev_node=prev_node,
37
+ csv_path=str(CSV_PATH),
38
+ )
39
+
40
+ nav_output = out["nav_output"]
41
+ plan = out["plan"] # ์—…๋ฐ์ดํŠธ๋œ PlanState ๊ณ„์† ์œ ์ง€
42
+ prev_node = nav_output["current_node"]
43
+
44
+ from pprint import pprint
45
+ print(f"\n[IMAGE] {img_name}")
46
+ pprint(nav_output)
47
+
48
+ if __name__ == "__main__":
49
+ quick_test()
uv.lock CHANGED
The diff for this file is too large to render. See raw diff
 
vercel.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "rewrites": [
3
+ { "source": "/(.*)", "destination": "/api" }
4
+ ]
5
+ }
6
+